Repository navigation
Fix: Play Video From Beginning doesn't override YouTube's async resume seek - #4350
Conversation
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
left a comment
There was a problem hiding this comment.
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.
|
hi and thanks! @akshar27 |
Problem
Fixes #4349
forcedPlayVideoFromTheBeginning()is called exactly once, synchronously, at the top ofinitPlayer(). On a full page load that's fine — by the time it runs, YouTube has already applied any saved-position resume, sovideo.currentTime > 1.1correctly 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 instantcurrentTimeis still ~0, so the> 1.1guard 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
seekedevent 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
tests/unit/play-video-from-beginning.test.jscovering: the function still exists and is wired intoinitPlayerfirst, the Videos not resuming after long pauses #262 double-play guard is preserved, the newseekedlistener 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.initPlayer/yt-navigate-finishcall sites.🤖 Generated with Claude Code
https://claude.ai/code/session_01Snzt7njUYkxw4oxYJsENbQ