Skip to content

fix(player): preserve native seeks after completion - #475

Merged
chhoumann merged 3 commits into
masterfrom
fix/position-edges
Oct 14, 2026
Merged

chhoumann merged 3 commits into
masterfrom
fix/position-edges

Conversation

@chhoumann

@chhoumann chhoumann commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

Summary

After an episode ended, Play could replace a native seek to 12 seconds with a restart at zero. Both Play paths now reconcile that movement first and treat the episode as paused. Untouched finished episodes still restart at zero, and unfinished synced progress still resumes.

Fixes #465
Refs #436

Verification

  • npm run lint, npm run format:check, npm run typecheck, and npm run build passed on Node 22.23.2.
  • npm run test passed 2,570 tests. npm run test:tooling passed 13 tests. npm run docs:build built 18 pages and resolved 112 legacy fragments.
  • npm run test:e2e passed 190 tests on each fresh Linux vault, using Obsidian 1.14.4 and 1.13.7. After a test-only review correction, an independent verifier passed the final nine focused journeys on 1.14.4 and the full 190 on 1.13.7. Four reproduction cases failed on master and passed after the fix. The fixture is Position Edges with a 60-second WAV. Screenshots use Fixtures/Local Fixture.wav.
  • npm run count:writes recorded three completed writes before and after the same listening session, with no failures. npm run size passed at 99,051 gzip bytes, up 14 bytes, below 100,000.

Verification report includes commands, runtime evidence, review decisions, and deviations. Reproduction scripts and logs include the mutation driver.

Before Play fix After Play fix
Seek replaced by 0 seconds Seek preserved at 12 seconds
All 11 wrong alternatives fail named tests
Wrong alternative Named failing test
no reconcile in play() synced progress handoff > play() of the same episode after a natural end starts at a seek from the browser's controls whose events have not arrived (#465)
no reconcile in resume() synced progress handoff > resume() after a natural end starts at a seek from the browser's controls whose events have not arrived (#465)
reconcile without normalizing ended to paused synced progress handoff > resume() after a natural end starts at a seek from the browser's controls whose events have not arrived (#465)
normalize only, no reconcile in play() or resume() synced progress handoff > resume() after a natural end starts at a seek from the browser's controls whose events have not arrived (#465)
play() reads its status before reconciling synced progress handoff > play() of the same episode after a natural end starts at a seek from the browser's controls whose events have not arrived (#465)
play() reconciles before validating its options synced progress handoff > play() rejecting a negative time after a natural end leaves the player as it was
target selection: ended && !moved, with restartTime honoring moved switching episodes (#353) > saves and queues an episode that a seek from the browser's controls moved after its natural end
ended Play uses the element's position synced progress handoff > resume() after a natural end continues a re-listen synced afterwards (#463)
a local move beats an explicit time synced progress handoff > play() with a time of 0 after a natural end starts there, not at a seek from the browser's controls
noteStoppedMove without holdsMedia() synced progress handoff > resume() before a reset element reports emptied preserves the position
noteStoppedMove checks a session instead of holdsMedia() synced progress handoff > resume() before a reset element reports emptied preserves the position

Why #436 remains open

A real quit lost 2.209345 seconds. A synchronous localStorage tail survived restart, but saved progress has no revision to compare with it. Adopting the tail into the progress store can override newer sync that arrives after startup.

A safe follow-up could keep a clean recovery candidate outside the store, compare the last completed writer base, and yield to later sync. Another option is atomic file replacement with verified quit coordination. Both need persistence work beyond this small player fix. QuickAdd prior art was checked. The existing guarantee of no new data.json write after quit remains intact.

Release impact

No schema, settings, public API, or URI changes. No migration. Production code changes only in the player controller.

Docs at release

Update docs/src/content/docs/podcasts.md to explain that a seek after completion wins on Play, an untouched finished episode restarts at zero, and unfinished synced progress resumes. Published docs remain unchanged in this PR.

Note

Fix PlayerController to preserve native seeks after natural end

  • When a media element's position changes after an episode ends naturally, PlayerController.play and PlayerController.resume now record that position before selecting the episode or restarting
  • The noteStoppedMove helper converts an ended state to paused when it sees a changed stopped position, so post-end movement is treated as active playback rather than an ended restart
  • Adds unit tests in PlayerController.test.ts covering seek-event timing, media resets, explicit play times, invalid play ranges, and episode switching
  • Adds end-to-end coverage in playback-position-edges.test.ts for four combinations of seek-event delivery and play initiation
  • Risk: resume now returns early for disposed controllers and no longer selects the ended restart branch when a post-end native seek occurs

Macroscope summarized c9e89fa.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: c9e89fa
Status: ✅  Deploy successful!
Preview URL: https://182ef0f6.podnotes.pages.dev
Branch Preview URL: https://fix-position-edges.podnotes.pages.dev

View logs

@chhoumann
chhoumann merged commit 8603ccf into master Oct 14, 2026
16 checks passed
@chhoumann
chhoumann deleted the fix/position-edges branch October 14, 2026 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Play after a seek on an ended episode restarts from zero

1 participant