Skip to content

save the listening position from the playback clock (PP-4964) - #233

Merged
mauricecarrier7 merged 1 commit into
mainfrom
fix/PP-4964-position-save-clock
Sep 29, 2026
Merged

mauricecarrier7 merged 1 commit into
mainfrom
fix/PP-4964-position-save-clock

Conversation

@mauricecarrier7

@mauricecarrier7 mauricecarrier7 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Jira: PP-4964 · follow-up PP-5268

What changes

The automatic listening-position save in AudiobookPlaybackModel now subscribes to player.positionPublisher, the stream every player feeds from its playback clock, with its own 15-second monotonic rate limit. It previously rode .positionUpdated from DefaultAudiobookManager's main-runloop Timer.publish, re-throttled on RunLoop.main. That is the kind of timer iOS coalesces during long screen-locked playback, and the reason the lock-screen heartbeat already moved to positionPublisher.

Drift is now measured from the last written position. The filter was given currentLocation, which positionPublisher has kept within a tick of the live playhead since #132, so it compared a timer sample against a position at most one timer interval newer. At 1× that gap stays under the 2-second threshold. This part is needed for the move to work at all: on the playback-clock stream the old comparison would see a gap of ~0 and never save.

Unchanged: lifecycle saves (resign active, background, termination, seek, chapter end, transport), save suppression, and the manager's timer, which still drives the lock screen and slider.

Why 15 seconds and not 5

Each write lands in Palace's TPPBookRegistry.setLocation, which rewrites the whole registry file twice (backup and primary) and posts a registry-changed notification that the shelf, holds and detail views observe. The remote side is already coalesced by RemotePositionWriter to one POST per 15 s per book, so a faster local cadence buys nothing on the server. At 15 s, each local write lines up with at most one POST, and the worst case after a kill is 15 s of listening. PP-5268 makes the local write cheap.

Measured (simulator, develop + ios-core #1526, Lyrasis Reads, unencrypted BiblioBoard title on OpenAccessPlayer, PP-4963 trace on, interim 5-second interval)

automatic saves kill → relaunch resumes
before 0 in ~9 min of continuous playback (only write: chapter completion) ~9 min behind the playhead
after 46 in 3m55s within 5 s

Verification (at 15 s)

  • New tests in AudiobookPlaybackModelAutosaveTests: the decision table (no baseline / inside interval / moved / not moved / suppressed / suppression lapsed) plus wiring, driven through PlayerMock._emitPosition with the manager's timer unable to produce a position. The wiring test pins the cadence at 11–12 writes per 180 s.
  • Reintroduction: deleting the autosaveFromPlaybackClock() call fails test_lockedListen_savesFromThePlaybackClockAlone; moving the baseline write above the suppression guard fails test_saveSuppression_whenItLapses_theNextDueTickWrites.
  • palace_mutate.py --diff-only: 2/2 killed, 0 errored.
  • Full toolkit suite via harness test --project audiobooktoolkit: 307 tests, 0 failures, 1 skipped (pre-existing).

Not done

  • No locked-screen measurement on hardware. The simulator's lock key left the app active, so it cannot reproduce iOS timer coalescing. The foreground defect above doesn't depend on locking.
  • ios-core submodule bump follows this merge.

🤖 Generated with Claude Code

The automatic position save rode `.positionUpdated` from
DefaultAudiobookManager's main-runloop `Timer.publish`, re-throttled on
`RunLoop.main`. iOS coalesces and suspends that kind of timer during long
screen-locked playback, which is why the lock-screen heartbeat was already
moved onto `player.positionPublisher`. The save now subscribes to the same
playback-clock stream, with its own 15-second monotonic rate limit.

Movement is now measured from the last WRITTEN position. The drift filter
was given `currentLocation`, which `positionPublisher` keeps within a tick
of the live playhead (since #132), so it compared a timer sample against a
position at most one timer interval newer. At 1x that gap stays under the
2-second threshold. Subscribing the old comparison to the playback clock
would have made the gap ~0 and saved nothing, so the two changes go
together.

15 seconds, not the 5 the old throttle named: each write reaches Palace's
TPPBookRegistry.setLocation, which rewrites the whole registry file twice
(backup and primary) and posts a registry-changed notification the shelf,
holds and detail views observe. The host's RemotePositionWriter already
coalesces to one POST per 15 seconds, so a faster local cadence would buy
nothing on the server either. PP-5268 makes the local write cheap.

Measured on a simulator, develop + ios-core #1526, Lyrasis Reads, an
unencrypted BiblioBoard audiobook (OpenAccessPlayer), trace switch on, at
the interim 5-second interval:

  before: 0 automatic saves in ~9 min of continuous playback (the only
          write was the chapter-completion save). Killing the app and
          relaunching resumed ~9 minutes behind the playhead.
  after:  46 saves in 3m55s. Kill and relaunch resumed within 5 seconds
          of the playhead.

At 15 seconds the worst case after a kill is 15 seconds of listening.

The first tick after load seeds the baseline instead of writing, so a
transient position emitted while the player settles cannot replace the
restored place. Save suppression still applies, and a suppressed tick does
not advance the baseline. Lifecycle saves (resign active, background,
termination, seek, chapter end, transport) are unchanged and remain the
backstop.

Verified by reintroduction at 15 seconds: deleting the
`autosaveFromPlaybackClock()` call fails
test_lockedListen_savesFromThePlaybackClockAlone; moving the baseline write
above the suppression guard fails
test_saveSuppression_whenItLapses_theNextDueTickWrites. palace_mutate
--diff-only: 2/2 killed, 0 errored. Full toolkit suite: 307 tests,
0 failures, 1 skipped (pre-existing).

**Scope:** AudiobookPlaybackModel only. DefaultAudiobookManager's timer
still publishes `.positionUpdated` for the lock screen and the slider; only
the save stopped listening to it.
**Not done:** a locked-screen measurement on physical hardware. The
simulator's lock key left the app `active` (no resign/background
notifications fired), so it cannot reproduce iOS timer coalescing and says
nothing about the locked half of the diagnosis. The foreground defect above
is independent of locking.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mauricecarrier7
mauricecarrier7 force-pushed the fix/PP-4964-position-save-clock branch from 018ae3e to c46c5dd Compare September 29, 2026 14:42
@mauricecarrier7
mauricecarrier7 merged commit 1b8c440 into main Sep 29, 2026
1 check passed
@mauricecarrier7
mauricecarrier7 deleted the fix/PP-4964-position-save-clock branch September 29, 2026 14:43
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.

1 participant