Repository navigation
fix(player): keep playing while you browse, with one player that owns the media - #401
Merged
Merged
Conversation
…iew closed, and across sync Before the player controller these journeys fail on master: browsing stops audio (#355), switching before the restored player opens zeroes its position (#353), commands and the API cannot play with the view closed, a link to a same-titled episode of another show resumes the loaded one, and a remote data.json change gets the local currentEpisode written back over it.
Plays the snapshot it is given: its own vault file, else a downloaded copy of the same media, else the stream. A missing file is an empty source, never other audio.
…achine The controller owns the single media element in the main window and exposes one read-only state plus intents. Only an explicit play selects an episode; restore, resume and progress never write settings.currentEpisode. Progress is saved only once the current load's metadata set the start position.
A range input snaps its value to the step from its minimum. Setting the value under the default step of 1 and then min 0.5 turned a playback rate of 1 into 1.5 on the rate slider.
…ores Every caller now plays, pauses, seeks and reads playback through the controller: the API, commands, URIs, menus, Play with PodNotes, the view and settings import. The player view only renders the controller's state, so audio keeps playing on other tabs, with the view closed and after a popout closes. Restoring at startup no longer autoplays or loads media, and playing another episode first no longer overwrites the restored one's position. The queue's reorder moves now name the episode instead of an index. A link to the loaded episode must name its feed or file as well as its title, and a link whose feed resolves after a newer play does not play.
…playback contract The journeys find the one media element by its class in the main window. J-PLAY-11 asserts the restored episode stays paused and then resumes from its saved position. The runtime helpers that faked the element are gone: links play real media and are checked within a tolerance. Closing a popout keeps the audio playing, and a reload right after keeps data.json intact. A synced selection now becomes this device's selection while what plays stays.
…rules The player keeps playing off its tab and with the view closed, and restores the last episode paused. The API page covers isPlaying while loading, length before metadata and seek validation; the URI page covers strict time parsing. The verify skill's player gotchas match the new behavior.
A video stage carries the media element into the popout's document, and the browser reloads a media element that changes documents: position 0, paused. The controller now reads and saves the element's position only while it holds the current load's media, and on an emptied it did not cause it parks the element back in the main window and starts it again where it was, playing if it played.
Closing the view no longer stops playback, so a playback checkpoint, or the position saved when PodNotes is disabled, could write data.json while the harness rewrote it, and the two writes interleaved into invalid JSON.
… event between them
…ks and the URI handler
…r, not the view (#94)
Deploying podnotes with
|
| Latest commit: |
8c9ea1a
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://9a6c59dd.podnotes.pages.dev |
| Branch Preview URL: | https://feat-player-controller.podnotes.pages.dev |
chhoumann
marked this pull request as ready for review
October 11, 2026 12:34
… error, and a finished episode's restart
# Conflicts: # src/URIHandler.test.ts # src/URIHandler.ts # src/main.ts # src/store/index.test.ts # src/ui/PodcastView/PodcastView.integration.test.ts # src/ui/PodcastView/PodcastView.svelte
… title-only progress (PB-06)
… is still loading
…found link against superseding a pending link
| // Taken once the link passed every check, so a refused link cancels nothing, | ||
| // and a newer link or play wins. | ||
| const request = ++latestRequest; | ||
| play = player.deferPlay(); |
There was a problem hiding this comment.
🟡 Medium src/URIHandler.ts:214
A feed request that fails after the user pauses, seeks, or starts another playback still shows “Could not load the podcast feed”; a title miss likewise shows “Episode not found” even though that player intent superseded the request. Those intents invalidate PlayerController’s deferred-play request, not latestRequest, and both notices are handled before play(...) checks that cancellation. Check the controller’s cancellation token before reporting fetch errors or title misses.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/URIHandler.ts around line 214:
A feed request that fails after the user pauses, seeks, or starts another playback still shows “Could not load the podcast feed”; a title miss likewise shows “Episode not found” even though that player intent superseded the request. Those intents invalidate `PlayerController`’s deferred-play request, not `latestRequest`, and both notices are handled before `play(...)` checks that cancellation. Check the controller’s cancellation token before reporting fetch errors or title misses.
This was referenced Oct 12, 2026
This was referenced Oct 13, 2026
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
Playback no longer depends on which PodNotes view is open. One
PlayerController(src/player/PlayerController.ts) owns the single media element in the main window from plugin load to unload, and the player view only renders its state and calls its intents.data.jsonchange synced from another device never loads, seeks, pauses or advances the queue. It also no longer gets this device's loaded episode written back over the other device's selection: only an explicit play, or the queue advancing at a natural end, writescurrentEpisode. While nothing is loaded, a sync refreshes the restored episode and position.timeandendTimein links parse strictly (12abcis refused),api.currentTime = NaNorundefinedis ignored, and a seek past the end stops 0.25 s short and pauses instead of finishing the episode.Every playback store is gone:
currentEpisode,currentTime,duration,isPaused,playbackRate,volume,requestedPlaybackTime,activePlaybackSegment,queue.playNext,subscribeQueueToCurrentEpisode, thecurrentEpisodepersistence binding and the republish after a sync. Episode identity stays the releasedgetEpisodeKey(podcast and title) and the Episode snapshot.Verification
Obsidian 1.13.7 on headless Linux (Xvfb), isolated worktree vault, Node v22.23.2. Feeds are the e2e fixture server and the verify skill's
PodNotes Fixture(http://127.0.0.1.nip.io:<port>/feed.xml, 60 s WAV enclosures); video checks used a 60 s WebM made with ffmpeg and "Play as video with PodNotes".Before (master
f7400ba) and after, same journeys. The new journeys intests/e2e/journey-playback.test.tsfail on master and pass on this branch:{0, 0}over 30 sdata.jsonchange during playbackcurrentEpisodewritten back over the remote oneThe journey "Start playing while a slow link to another show loads keeps playing the paused episode" reproduces V-401's finding: run against the pre-fix controller (head
58b77ee) it fails with the other show's episode playing after the delayed feed arrives; it passes on this head.Recordings with an on-screen probe of
api.isPlayingandapi.currentTime:api.stop()pauses it.Also checked in Obsidian: headset previous-track capture (existing e2e, with the view closed), a popout holding the view closes while audio keeps playing and
data.jsonstays intact, a reload right after (#370), hiding the main window saves the position at once, a video moved to a popout keeps playing in the main window while the popout shows the artwork, andapi.start(); api.stop()in one call leaves the media paused.Commands on the head commit:
git grep -nE "currentTime\.set|isPaused|requestedPlaybackTime|activePlaybackSegment|subscribeQueueToCurrentEpisode|playNext|currentEpisode\.(set|subscribe)|bind:currentTime|bind:paused|<audio|<video" -- srcprints nothing: the view-owned media element and its playback store wiring are gone.Reviewed by an adversarial panel (Claude Opus 5.5 and GPT-6 Astra) over eight rounds on the full diff and each fix diff, and verified independently (V-401 and V-401b, whose findings this head fixes); every accepted finding has a test that fails without its fix.
Release impact
None for storage: no
data.jsonshape change and no migration.settings.currentEpisodeis now written only by an explicit selection, so after a sync it keeps the other device's selection instead of this device's loaded episode. The public API (src/API/IAPI.ts) andobsidian://podnotesparameters are unchanged. Behavior differences a user can notice: closing the PodNotes view or a popout no longer stops audio (use Stop or Toggle playback); a seek past the end pauses just before it;isPlayingis also true while an episode is loading.Fixes #355
Fixes #353
Fixes #377
Note
Replace playback stores with a single
PlayerControllerthat keeps playing while browsingPlayerControllerin PlayerController.ts, which owns one main-window media element and one reactive player state. Playback now continues while browsing views, changing settings, or closing the player viewEpisodePlayer, settings import — now route through the controllerselectSourcein source.ts, which picks the episode's vault snapshot first, then a matching download, then the stream URL; a missing vault file stays unplayableFakeMediamockAPI.isPlayingis true while media loads; forward skips are capped 0.25s before the end and keep playing; importing settings updates the live player's volume and rateMacroscope summarized 8c9ea1a.