Skip to content

Fix: Play Video From Beginning doesn't override YouTube's async resume seek - #4350

Merged
ImprovedTube2 merged 1 commit into
code-charity:masterfrom
akshar27:fix/resume-playback-from-beginning
Sep 26, 2026
Merged

ImprovedTube2 merged 1 commit into
code-charity:masterfrom
akshar27:fix/resume-playback-from-beginning

Conversation

@akshar27

Copy link
Copy Markdown
Contributor

Problem

Fixes #4349

forcedPlayVideoFromTheBeginning() is called exactly once, synchronously, at the top of initPlayer(). On a full page load that's fine — by the time it runs, YouTube has already applied any saved-position resume, so video.currentTime > 1.1 correctly detects it and forces a restart to 0.

On SPA-style navigations, though (playlist auto-advance to the next video, or clicking a previously watched video from recommendations/history), YouTube applies its own "resume to last watched position" seek asynchronously, after initPlayer()'s single synchronous check already ran. At that instant currentTime is still ~0, so the > 1.1 guard doesn't fire, no override happens, and by the time YouTube's real resume seek lands a moment later, the check has already run and won't run again — the video silently resumes from the saved position instead of restarting from the beginning. This matches the report exactly: "happens when watching a channel's playlist, and when clicking on a video that I previously watched."

Fix

Keep the existing synchronous check (handles the normal page-load case), and add a bounded, one-shot seeked event listener that catches YouTube's late resume seek if/when it actually happens, within a short grace window (2s), then re-applies the same override logic. The listener bails out if the page has since navigated to a different video. This reacts to the actual resume event rather than blindly re-polling after a fixed delay, which would risk misinterpreting organic playback progress as a resume seek.

The existing double-play guard for freshly-started videos (currentTime > 1.1, see #262) and the timestamped-URL early return are both unchanged.

Testing

  • Added tests/unit/play-video-from-beginning.test.js covering: the function still exists and is wired into initPlayer first, the Videos not resuming after long pauses #262 double-play guard is preserved, the new seeked listener is attached/removed correctly, the grace window is bounded, and the listener bails out on navigation to a different video.
  • npm test — all 122 tests pass (122/122, including the 6 new ones).
  • npm run lint — clean, no errors.
  • Not manually verified against live YouTube in a loaded browser instance (dev-mode extension load into a real browser profile was skipped for this change); verification here is via the unit test suite plus manual code review of the exact race condition against the existing initPlayer/yt-navigate-finish call sites.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Snzt7njUYkxw4oxYJsENbQ

forcedPlayVideoFromTheBeginning() only ran its currentTime>1.1 check
once, synchronously, at the top of initPlayer(). On SPA navigations
(playlist auto-advance, clicking a previously watched video) YouTube
applies its own resume-to-saved-position seek asynchronously, after
that single check already ran with currentTime still ~0 - so the
override never fired and the video silently resumed instead of
restarting from 0.

Add a bounded one-shot 'seeked' listener that catches YouTube's late
resume seek within a short grace window and re-applies the override,
without blindly re-polling (which could misfire against organic
playback progress). Existing double-play guard (code-charity#262) and timestamp
URL early-return are unchanged.

Fixes code-charity#4349

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Snzt7njUYkxw4oxYJsENbQ

@wahajahmed010 wahajahmed010 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Textbook SPA race-condition fix. The diagnosis is precise (synchronous check misses the async resume seek) and the implementation uses an event-listener + bounded timer, which is exactly the right pattern. A few notes:

1. The dual-trigger approach is correct. You re-check on seeked (catches the actual late seek) and also clear after 2s (bounds the listener lifetime). If only seeked were used, no seeked event would leak a listener forever on a page where YouTube never seeks (e.g., a brand-new video). Good defensive coding.

2. settled flag prevents double-fire. Without it, both the seeked listener and the setTimeout could race to remove the listener after the seek lands. Good.

3. Guard this.video_url !== navigatedUrl is essential. Without it, navigating to a different video within the 2s window would carry the old listener onto a fresh <video> element and could mistakenly seek that video. Good.

4. Magic number: 2000ms. The choice isn't justified in the comment. Why 2s and not 1s or 5s? YouTube's resume-seek is typically within a few hundred milliseconds of initPlayer running, but on slow connections or under heavy main-thread load it could plausibly take longer. Suggest adding a one-line justification (e.g., "// 2s empirically covers late resume-seek on slow connections; longer risks leaking listeners on slow SPA transitions").

5. Possible cleanup nit. If video.seeked already fired (race window: the seek lands before addEventListener is attached), the listener will wait the full 2s before being removed. Not a correctness bug, just a small resource hold. Could be addressed by checking video.currentTime > 1.1 immediately after addEventListener, but that's adding complexity for marginal gain. Leave as-is.

6. Test coverage is shallow. The new tests/unit/play-video-from-beginning.test.js checks for the presence of substrings (expect(playerContent).toContain(...)). This catches regressions where the listener is removed entirely, but won't catch a logic bug, e.g., if someone writes video.removeEventListener('seeked', onSeeked) but with the wrong function reference. For a logic-level fix this thin, a regex/string-match test is weak. The codebase doesn't have a JS runtime harness for this script, so a true functional test is hard, but at minimum an eval + jsdom harness would catch more than substring matching.

7. Pre-existing issue: paused was captured at function entry, but paused is now redundant inside the function. You correctly moved to capturing video.paused inside forceRestart() so the paused-state reflects after the seek, which is what you want. Good catch.

8. t from this.video_url.match(this.regex.video_time)?.[1] is still consumed above. Unchanged, just confirming I read the diff correctly. No regression on the existing timestamp-extraction path.

Not blocking. The fix solves the reported race and the bounded listener is the right shape. Address the 2000ms justification and consider upgrading the test depth for follow-up.

@ImprovedTube2 ImprovedTube2 added the untested please test. (also applies to proactively merged pull requests.) label Sep 24, 2026
@ImprovedTube2

Copy link
Copy Markdown
Collaborator

hi and thanks! @akshar27

@ImprovedTube2
ImprovedTube2 merged commit 1bcda82 into code-charity:master Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

untested please test. (also applies to proactively merged pull requests.)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐞Play Video From Beginning

3 participants