Function
The stored position, or null while unread or absent
The open offer and the three actions that answer it
The offer is made once per mount, and only once the learner's first
play has actually begun. Landing on a lesson prompts nothing: the learner
may have come for the notes. When playback starts, and the saved position
clears isPositionResumable, the player is paused and the position is
offered.
The hold happens on playback start, not on the play request, and the
distinction is the whole reason this hook has a handlePlaybackStarted
rather than a handlePlay. A provider that drives a third-party embed can
swallow a pause issued while its initial play request is still in flight
and stall there for good — never emitting pause, never reaching
playing, ignoring every later seek and play. Waiting for playback to
begin puts the pause outside that window for every provider, at the cost of
a few milliseconds of video. See design.md §D1 of
fix-youtube-resume-stuck-buffering.
Every answer ends with the video playing, because playback starting is
what opened the overlay. Resuming seeks to the saved position; restarting —
which is also where dismissal lands — seeks to 0. Either way the offer is
spent and later plays go straight through.
The saved position is read asynchronously by the caller, so it may arrive after playback has started. It is not offered retroactively: interrupting a learner who is already watching is worse than skipping the offer.
const resume = useResumeOnFirstPlay({ savedPositionSeconds, durationSeconds, player });
<MediaPlayer onPlaying={resume.handlePlaybackStarted}>
{resume.offeredSeconds !== null ? (
<LessonVideoResumeOverlay
positionSeconds={resume.offeredSeconds}
onResume={resume.resumeFromSavedPosition}
onRestart={resume.restartFromBeginning}
/>
) : null}
</MediaPlayer>
The "offer to resume on the first play" rule, with no player library in it.