Skip to content

fix(speed): preserve native rate when extension has no authoritative target#1537

Open
rdavidwu wants to merge 1 commit into
igrigorik:masterfrom
rdavidwu:rdavidwu/native-speed-reset-bug
Open

fix(speed): preserve native rate when extension has no authoritative target#1537
rdavidwu wants to merge 1 commit into
igrigorik:masterfrom
rdavidwu:rdavidwu/native-speed-reset-bug

Conversation

@rdavidwu

@rdavidwu rdavidwu commented May 5, 2026

Copy link
Copy Markdown

Summary

Fixes a bug where Chrome's native HTML5 playback-speed control gets silently undone when the extension is loaded.

Reproduction (current behavior):

  1. Open a plain <video> page with the extension installed (e.g., w3schools video sample) on a fresh browser session — lastSpeed=null, no per-site rule, rememberSpeed=off.
  2. Right-click → Show controls → three-dot menu → Playback speed → 1.5x. Video plays at 1.5x.
  3. Click pause, then play. Or scrub the timeline.
  4. Bug: speed silently drops to 1.0 while the native menu still shows 1.5x.

Root cause: When lastSpeed === null and no siteDefaultSpeed rule applies, getTargetSpeed() returns the 1.0 baseline as a fallback. Lifecycle handlers (mediaEventAction on play/seeked, and initializeSpeed's deferred loadedmetadata handler) then call adjustSpeed(video, 1.0, init), force-overwriting whatever rate the user set via native controls.

Fix: Add an early-return guard at the top of mediaEventAction and initializeSpeed: when the extension has no authoritative target (lastSpeed null AND siteDefaultSpeed unset), do nothing. The video keeps whatever rate it has. This preserves the null = no opinion invariant established by 12a4fcd end-to-end.

The fix targets the lifecycle restore path rather than trying to detect user gestures upstream — gesture-detection in event-manager.js would require a DOM click to reach document during Chrome's native context-menu flow, which isn't guaranteed (clicks dispatch in browser chrome) and would also widen the trust boundary in ways that could regress 12a4fcd's protection against autonomous site rate changes (ad transitions, stream switches).

Test Coverage

                    mediaEventAction(event) / initializeSpeed()
                              │
                              ▼
              ┌───────────────────────────────┐
              │ lastSpeed === null  &&        │  ← NEW guard
              │ siteDefaultSpeed null/undef ? │
              └──────────────┬────────────────┘
                  TRUE       │       FALSE
            ┌───────────────┘└────────────────┐
            ▼                                  ▼
    [debug log + return]              getTargetSpeed(...)
    native rate preserved              adjustSpeed(...) (existing)
            │                                  │
   ┌────────┼────────┐                ┌────────┴──────────┐
   ▼        ▼        ▼                ▼                   ▼
  play    seeked   loadedmd       lastSpeed set      site rule set
   │        │        │                │                   │
  [T1]    [T2]     [T3]           [#1494]         [event-manager.js]
   ✓        ✓        ✓                ✓                   ✓

Three new tests in tests/unit/core/video-controller.test.js:

  • play event is no-op when lastSpeed is null and no per-site rule
  • seeked event is no-op when lastSpeed is null and no per-site rule
  • initializeSpeed is no-op when lastSpeed is null and no per-site rule (deferred init path)

Existing tests unchanged and still passing:

Tests: 377 → 378 (+1 for the deferred-init regression). All 378 vitest tests pass.

Manual verification

  1. npm run build and load dist/ unpacked in Chrome.
  2. Open https://www.w3schools.com/html/html5_video.asp (or any plain <video> page) with lastSpeed=null and rememberSpeed=off.
  3. Native context menu → Playback speed → 1.5x. Video plays at 1.5x.
  4. Click pause then play, then scrub the timeline. Expected: rate stays 1.5x. (Without this fix: drops to 1.0.)
  5. Repeat with rememberSpeed on (lastSpeed previously set to 1.8): the existing Regression: keyboard s/d shortcuts use 1.0x baseline after tab backgrounding #1494 behavior is preserved — rate is restored to 1.8 on play even after a background-tab reset.

Test plan

🤖 Generated with Claude Code

…target

When lastSpeed is null and no per-site rule applies, lifecycle restore
on play, seeked, or loadedmetadata silently overwrote the video's current
playbackRate with the 1.0 baseline. This dropped any speed the user set
via Chrome's native HTML5 controls (or that a site set pre-metadata-load)
the moment the user paused, played, or scrubbed.

Add an early-return guard at the top of mediaEventAction and
initializeSpeed: when lastSpeed is null AND siteDefaultSpeed is unset,
the extension has no opinion and must not act. This preserves the
"null = no opinion" invariant established by 12a4fcd end-to-end.

Three regression tests cover the play, seeked, and deferred
loadedmetadata paths. The existing igrigorik#1494 regression and event-manager
gesture tests are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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