diff --git a/extension/content/inject.js b/extension/content/inject.js index b12447d..00831ea 100644 --- a/extension/content/inject.js +++ b/extension/content/inject.js @@ -3144,6 +3144,12 @@ function setupControllerHostTracking(videoController, wrapper, mount) { resizeObserver.observe(geometryMount); } + var mediaGeometryEvents = ["loadedmetadata", "play", "playing"]; + mediaGeometryEvents.forEach(function(eventName) { + videoController.video.addEventListener(eventName, schedule, { + passive: true + }); + }); win.addEventListener("resize", schedule, { passive: true }); doc.addEventListener("fullscreenchange", schedule, { passive: true }); geometryMount.addEventListener("scroll", schedule, { passive: true }); @@ -3154,6 +3160,9 @@ function setupControllerHostTracking(videoController, wrapper, mount) { if (resizeObserver) resizeObserver.disconnect(); if (frameId !== null) win.cancelAnimationFrame(frameId); if (geometryRetryTimer !== null) win.clearTimeout(geometryRetryTimer); + mediaGeometryEvents.forEach(function(eventName) { + videoController.video.removeEventListener(eventName, schedule); + }); win.removeEventListener("resize", schedule); doc.removeEventListener("fullscreenchange", schedule); geometryMount.removeEventListener("scroll", schedule); @@ -3329,8 +3338,15 @@ function syncControllerFullscreenMount(videoController) { (fullscreenElement === video || isComposedDescendant(video, fullscreenElement)) ); + var normalGeometryMount = getControllerGeometryMount(targetMount); + var normalMountIsAlreadyFullscreenVisible = Boolean( + fullscreenElement && + fullscreenElement !== video && + normalGeometryMount && + isComposedDescendant(normalGeometryMount, fullscreenElement) + ); - if (ownsFullscreen) { + if (ownsFullscreen && !normalMountIsAlreadyFullscreenVisible) { targetMount = getControllerMount(video, fullscreenElement); } else if (!fullscreenElement && (!targetMount || !targetMount.isConnected)) { targetMount = getControllerMount(video); @@ -3950,6 +3966,9 @@ function defineVideoController() { const speed = this.video.playbackRate.toFixed(2); var wrapper = doc.createElement("div"); wrapper.classList.add("vsc-controller"); + // Keep the host out of player layout while its shadow stylesheet loads. + wrapper.style.position = "absolute"; + wrapper.style.pointerEvents = "none"; if (!hasUsableMediaSource(this.video)) wrapper.classList.add("vsc-nosource"); if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden"); diff --git a/extension/content/shadow.css b/extension/content/shadow.css index ac542a0..f25f323 100644 --- a/extension/content/shadow.css +++ b/extension/content/shadow.css @@ -1,9 +1,5 @@ :host { position: absolute !important; - top: 0 !important; - left: 0 !important; - width: 100% !important; - height: 100% !important; pointer-events: none !important; z-index: 2147483647 !important; white-space: normal; @@ -15,7 +11,6 @@ base absolute host rule. */ :host(.vsc-fullscreen-popover) { position: fixed !important; - inset: auto !important; margin: 0 !important; padding: 0 !important; border: 0 !important; diff --git a/tests/inject-lifecycle.regression.test.js b/tests/inject-lifecycle.regression.test.js index 7b5d52d..b847272 100644 --- a/tests/inject-lifecycle.regression.test.js +++ b/tests/inject-lifecycle.regression.test.js @@ -2,7 +2,8 @@ const { createChromeMock, evaluateScript, flushAsyncWork, - loadHtmlString + loadHtmlString, + readWorkspaceFile } = require("./helpers/extension-test-utils"); function bootInject(options) { @@ -280,6 +281,45 @@ describe("inject.js media/controller lifecycle regressions", () => { expect(video.vsc.div.style.getPropertyValue("height")).toBe("226px"); }); + it("does not let shadow host defaults override measured geometry", () => { + const shadowCss = readWorkspaceFile("extension/content/shadow.css"); + const hostRule = shadowCss.match(/:host\s*\{([^}]*)\}/)[1]; + const fullscreenRule = shadowCss.match( + /:host\(\.vsc-fullscreen-popover\)\s*\{([^}]*)\}/ + )[1]; + + expect(hostRule).not.toMatch(/\b(?:top|left|width|height)\s*:/); + expect(fullscreenRule).not.toMatch(/\binset\s*:/); + }); + + it("repositions a Shorts controller when playback moves the video on-screen", async () => { + vi.useFakeTimers(); + bootInject({ url: "https://www.youtube.com/shorts/example" }); + await settleLifecycle(); + + const player = document.createElement("div"); + player.className = "html5-video-player"; + const video = document.createElement("video"); + const playerRect = makeRect(40, 64, 351, 624); + let videoRect = makeRect(40, -560, 351, 624); + + setRect(player, playerRect); + setBoxMetrics(player, playerRect.width, playerRect.height); + video.getBoundingClientRect = () => videoRect; + video.src = "blob:https://www.youtube.com/shorts"; + player.appendChild(video); + document.body.appendChild(player); + + window.ensureController(video, player); + expect(video.vsc.div.style.getPropertyValue("top")).toBe("-624px"); + + videoRect = playerRect; + video.dispatchEvent(new Event("playing")); + await vi.advanceTimersByTimeAsync(0); + + expect(video.vsc.div.style.getPropertyValue("top")).toBe("0px"); + }); + it("targets the controller nearest the pointer unless change-all is selected", async () => { bootInject(); await settleLifecycle(); @@ -638,6 +678,25 @@ describe("inject.js media/controller lifecycle regressions", () => { controller.controllerHostCleanup(); }); + it("keeps a visible player-local host when Firefox fullscreens the page root", async () => { + bootInject(); + await settleLifecycle(); + + const { mount, controller, wrapper } = createControlledVideo(); + setRect(document.documentElement, makeRect(0, 0, 0, 0)); + Object.defineProperty(document, "fullscreenElement", { + configurable: true, + value: document.documentElement + }); + + window.syncControllerFullscreenMount(controller); + + expect(wrapper.parentElement).toBe(mount); + expect(wrapper.classList.contains("vsc-geometry-hidden")).toBe(false); + expect(document.documentElement.style.position).toBe(""); + expect(document.documentElement.style.isolation).toBe(""); + }); + it("only promotes the directly-fullscreen video's controller", async () => { bootInject(); await settleLifecycle();