Problem
TC-01/TC-07 (scripts/test-extension-e2e.mjs, Firefox e2e, playback-speed
assertions) flake intermittently — alternating which one fails across runs,
never both together, while TC-17/TC-18 (queue-overlay, same content script)
pass consistently. Root-caused by code inspection, not yet fixed in code per
K's direction (investigate + file issue only, no implementation this pass).
Root cause
tryInitVideo (apps/shared/src/content/content.js) is a one-shot,
debounced (300ms) call to findVideoElement(document) =
document.querySelector('video'). It runs exactly once per trigger with no
retry:
- On cold load: called unconditionally at the end of
init() if
isYouTubeWatchPage(location.href). Content script runs at
document_idle (apps/firefox-extension/manifest.json), which guarantees
the initial HTML parse finished, not that YouTube's client-rendered
<video> element already exists. If it appears after the 300ms debounce
window closes, tryInitVideo finds nothing and never runs again for that
page load.
- The only other trigger,
siteAdapter.onNavigate()
(src/site-adapters/youtube/youtube-site-adapter.js), listens for
yt-navigate-finish but ignores same-id events: lastId is captured from
window.location.href at registration time, which on a cold/hard
navigation already equals the target page's id. So YouTube's own
hydration-completion signal (same id, later timing) can never serve as a
retry — the callback only fires on an actual page-to-page SPA
navigation.
Compounding factor: createPlayerAdapter (src/player-adapters/html5-video-player-adapter.js)
binds to one specific DOM node with no rebind path if YouTube swaps the
<video> element later (e.g. ad transition). isReady() is defined
(videoElement.readyState >= 1) but is dead code — no call sites anywhere
in the repo — suggesting a readiness-gating mechanism was planned but never
wired in.
Proposed fix
Add a bounded MutationObserver fallback so tryInitVideo catches a
late-appearing <video> element instead of giving up after one check:
- On the same trigger paths as today (
init()'s cold-load call and
onNavigate()'s callback), if findVideoElement finds nothing, start
observing (childList + subtree) a narrow-as-possible root — ideally a
YouTube app container rather than document.body — for mutations, and
re-run findVideoElement on each mutation batch.
- On success: disconnect the observer immediately, then proceed with the
existing createPlayerAdapter(el) setup.
- On a new navigation cycle starting (
onNavigate firing again) or a bound
attempt-count/timeout being exceeded: disconnect the observer instead of
leaving it running past its useful window.
- Possibly revive
isReady() as part of the found-element check instead of
just existence, since a freshly-inserted <video> node can still be at
readyState 0.
Open doubts
- What to observe:
document.body is simplest but fires on every DOM churn
YouTube does (a lot, on a video-heavy SPA); a narrower YouTube-specific
container (e.g. #movie_player or ytd-app) would cut mutation-callback
volume but needs confirming it exists early enough itself, and isn't
itself replaced across navigations.
- Whether to cap observation with a timeout/attempt count, or let it run
until the next navigation resets it — an unbounded observer on a page
where the video genuinely never loads (e.g. removed/private video) would
sit connected doing nothing useful indefinitely.
- How this interacts with the existing
onNavigate/yt-navigate-finish
path so the two retry mechanisms don't fight each other or double-invoke
tryInitVideo for the same page load.
- Whether reviving
isReady() is in scope here or a separate concern —
finding the node is not the same problem as the node being ready to read
playbackRate from.
Memory-efficiency constraint
Must not accumulate MutationObserver instances over a long-lived tab's
session. YouTube is an SPA — a single real tab can fire many
yt-navigate-finish navigations without the content script ever being
re-injected, unlike the e2e suite's hard-navigation-per-test-case pattern
(navigate() in scripts/test-extension-e2e.mjs does a full
window.location.href reload, which is not representative of real usage
here). The fix needs:
- At most one live observer per content-script instance at any time — reuse
a single observer reference (disconnect + reconnect, or just leave
disconnected until needed) rather than new MutationObserver on every
tryInitVideo invocation.
- Guaranteed disconnect on the success path and on navigation-reset, so cost
doesn't grow with number of navigations within one page/tab.
- Total added cost should scale with number of open tabs/pages (one
content-script instance each), not with number of navigations any single
tab makes over its lifetime.
Non-goals for this issue
No code changes here — investigation + fix proposal only, per K's
direction. Implementation is a separate follow-up once the open doubts
above are resolved.
Relates to docs/specs/keyboard-shortcuts.md (E2E section, TC-01/TC-07).
Problem
TC-01/TC-07 (
scripts/test-extension-e2e.mjs, Firefox e2e, playback-speedassertions) flake intermittently — alternating which one fails across runs,
never both together, while TC-17/TC-18 (queue-overlay, same content script)
pass consistently. Root-caused by code inspection, not yet fixed in code per
K's direction (investigate + file issue only, no implementation this pass).
Root cause
tryInitVideo(apps/shared/src/content/content.js) is a one-shot,debounced (300ms) call tofindVideoElement(document)=document.querySelector('video'). It runs exactly once per trigger with noretry:
init()ifisYouTubeWatchPage(location.href). Content script runs atdocument_idle(apps/firefox-extension/manifest.json), which guaranteesthe initial HTML parse finished, not that YouTube's client-rendered
<video>element already exists. If it appears after the 300ms debouncewindow closes,
tryInitVideofinds nothing and never runs again for thatpage load.
siteAdapter.onNavigate()(
src/site-adapters/youtube/youtube-site-adapter.js), listens foryt-navigate-finishbut ignores same-id events:lastIdis captured fromwindow.location.hrefat registration time, which on a cold/hardnavigation already equals the target page's id. So YouTube's own
hydration-completion signal (same id, later timing) can never serve as a
retry — the callback only fires on an actual page-to-page SPA
navigation.
Compounding factor:
createPlayerAdapter(src/player-adapters/html5-video-player-adapter.js)binds to one specific DOM node with no rebind path if YouTube swaps the
<video>element later (e.g. ad transition).isReady()is defined(
videoElement.readyState >= 1) but is dead code — no call sites anywherein the repo — suggesting a readiness-gating mechanism was planned but never
wired in.
Proposed fix
Add a bounded
MutationObserverfallback sotryInitVideocatches alate-appearing
<video>element instead of giving up after one check:init()'s cold-load call andonNavigate()'s callback), iffindVideoElementfinds nothing, startobserving (
childList+subtree) a narrow-as-possible root — ideally aYouTube app container rather than
document.body— for mutations, andre-run
findVideoElementon each mutation batch.existing
createPlayerAdapter(el)setup.onNavigatefiring again) or a boundattempt-count/timeout being exceeded: disconnect the observer instead of
leaving it running past its useful window.
isReady()as part of the found-element check instead ofjust existence, since a freshly-inserted
<video>node can still be atreadyState 0.Open doubts
document.bodyis simplest but fires on every DOM churnYouTube does (a lot, on a video-heavy SPA); a narrower YouTube-specific
container (e.g.
#movie_playerorytd-app) would cut mutation-callbackvolume but needs confirming it exists early enough itself, and isn't
itself replaced across navigations.
until the next navigation resets it — an unbounded observer on a page
where the video genuinely never loads (e.g. removed/private video) would
sit connected doing nothing useful indefinitely.
onNavigate/yt-navigate-finishpath so the two retry mechanisms don't fight each other or double-invoke
tryInitVideofor the same page load.isReady()is in scope here or a separate concern —finding the node is not the same problem as the node being ready to read
playbackRatefrom.Memory-efficiency constraint
Must not accumulate
MutationObserverinstances over a long-lived tab'ssession. YouTube is an SPA — a single real tab can fire many
yt-navigate-finishnavigations without the content script ever beingre-injected, unlike the e2e suite's hard-navigation-per-test-case pattern
(
navigate()inscripts/test-extension-e2e.mjsdoes a fullwindow.location.hrefreload, which is not representative of real usagehere). The fix needs:
a single observer reference (disconnect + reconnect, or just leave
disconnected until needed) rather than
new MutationObserveron everytryInitVideoinvocation.doesn't grow with number of navigations within one page/tab.
content-script instance each), not with number of navigations any single
tab makes over its lifetime.
Non-goals for this issue
No code changes here — investigation + fix proposal only, per K's
direction. Implementation is a separate follow-up once the open doubts
above are resolved.
Relates to
docs/specs/keyboard-shortcuts.md(E2E section, TC-01/TC-07).