morgan pushed to branch tor-browser-153.1.0esr-16.0-1 at The Tor Project / Applications / Tor Browser Commits: 3c9945e2 by Pier Angelo Vendrame at 2026-08-24T13:35:37+00:00 Bug 2063031 - Spoof video PiP size under RFP. r=media-playback-reviewers,kpatenio,alwu,tjr Differential Revision: https://phabricator.services.mozilla.com/D318184 - - - - - 6 changed files: - dom/media/PictureInPictureWindow.cpp - dom/media/PictureInPictureWindow.h - dom/media/mediaelement/HTMLVideoElement.cpp - dom/media/mediaelement/HTMLVideoElement.h - toolkit/components/pictureinpicture/tests/browser.toml - + toolkit/components/pictureinpicture/tests/browser_resistFingerprinting.js Changes: ===================================== dom/media/PictureInPictureWindow.cpp ===================================== @@ -47,6 +47,11 @@ int32_t PictureInPictureWindow::Width() const { if (!IsStateOpened()) { return 0; } + RefPtr<HTMLVideoElement> videoElement = mAssociatedVideoElement.get(); + if (videoElement && videoElement->OwnerDoc()->ShouldResistFingerprinting( + RFPTarget::ScreenRect)) { + return VideoSizeForRFP().width; + } return mWidth; } @@ -57,14 +62,54 @@ int32_t PictureInPictureWindow::Height() const { if (!IsStateOpened()) { return 0; } + RefPtr<HTMLVideoElement> videoElement = mAssociatedVideoElement.get(); + if (videoElement && videoElement->OwnerDoc()->ShouldResistFingerprinting( + RFPTarget::ScreenRect)) { + return VideoSizeForRFP().height; + } return mHeight; } +gfx::IntSize PictureInPictureWindow::VideoSizeForRFP() const { + RefPtr<HTMLVideoElement> videoElement = mAssociatedVideoElement.get(); + if (!videoElement) { + return {0, 0}; + } + + // From PictureInPicture.sys.mjs: "The Picture in Picture window will be a + // maximum of a quarter of the screen height, and a third of the screen + // width.". + // Pretend we are maximizing the video in a 1920x1080 display. + const uint32_t maxWidth = 1920 / 3; + const uint32_t maxHeight = 1080 / 4; + uint32_t width = videoElement->VideoWidth(); + uint32_t height = videoElement->VideoHeight(); + if ((height > maxHeight || width > maxWidth) && height > 0) { + double aspectRatio = static_cast<double>(width) / height; + if (width >= height) { + width = maxWidth; + height = static_cast<uint32_t>(round(maxWidth / aspectRatio)); + } else { + height = maxHeight; + width = static_cast<uint32_t>(round(maxHeight * aspectRatio)); + } + } + return {width, height}; +} + void PictureInPictureWindow::NotifyDimensionsChanged(int32_t aWidth, int32_t aHeight) { mWidth = aWidth; mHeight = aHeight; + RefPtr<HTMLVideoElement> videoElement = mAssociatedVideoElement.get(); + if (videoElement && videoElement->OwnerDoc()->ShouldResistFingerprinting( + RFPTarget::ScreenRect)) { + // With RFP, we spoof the window size to a fixed size that depends on the + // video, therefore it does not make sense to trigger a resize event. + return; + } + // When the size of a Picture-in-Picture window pipWindow changes, // the user agent MUST queue a task to fire an event named resize at // pipWindow. ===================================== dom/media/PictureInPictureWindow.h ===================================== @@ -47,6 +47,8 @@ class PictureInPictureWindow final : public DOMEventTargetHelper { private: bool IsStateOpened() const { return mOpened; } + gfx::IntSize VideoSizeForRFP() const; + WeakPtr<HTMLVideoElement> mAssociatedVideoElement; int32_t mWidth = 0; int32_t mHeight = 0; ===================================== dom/media/mediaelement/HTMLVideoElement.cpp ===================================== @@ -315,7 +315,7 @@ bool HTMLVideoElement::IsInteractiveHTMLContent() const { HTMLMediaElement::IsInteractiveHTMLContent(); } -gfx::IntSize HTMLVideoElement::GetVideoIntrinsicDimensions() { +gfx::IntSize HTMLVideoElement::GetVideoIntrinsicDimensions() const { const auto& sz = mMediaInfo.mVideo.mDisplay; // Prefer the size of the container as it's more up to date. @@ -324,7 +324,7 @@ gfx::IntSize HTMLVideoElement::GetVideoIntrinsicDimensions() { .valueOr(sz); } -uint32_t HTMLVideoElement::VideoWidth() { +uint32_t HTMLVideoElement::VideoWidth() const { if (!HasVideo()) { return 0; } @@ -336,7 +336,7 @@ uint32_t HTMLVideoElement::VideoWidth() { return size.width; } -uint32_t HTMLVideoElement::VideoHeight() { +uint32_t HTMLVideoElement::VideoHeight() const { if (!HasVideo()) { return 0; } ===================================== dom/media/mediaelement/HTMLVideoElement.h ===================================== @@ -96,9 +96,9 @@ class HTMLVideoElement final : public HTMLMediaElement { SetUnsignedIntAttr(nsGkAtoms::height, aValue, 0, aRv); } - uint32_t VideoWidth(); + uint32_t VideoWidth() const; - uint32_t VideoHeight(); + uint32_t VideoHeight() const; VideoRotation RotationDegrees() const { return mMediaInfo.mVideo.mRotation; } @@ -179,7 +179,7 @@ class HTMLVideoElement final : public HTMLMediaElement { void CreateVideoWakeLockIfNeeded(); void ReleaseVideoWakeLockIfExists(); - gfx::IntSize GetVideoIntrinsicDimensions(); + gfx::IntSize GetVideoIntrinsicDimensions() const; RefPtr<WakeLock> mScreenWakeLock; ===================================== toolkit/components/pictureinpicture/tests/browser.toml ===================================== @@ -158,6 +158,8 @@ support-files = ["test-page-with-nan-video-duration.html"] ["browser_removeVideoElement.js"] +["browser_resistFingerprinting.js"] + ["browser_resizeVideo.js"] skip-if = [ "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11'", # Bug 1594223 ===================================== toolkit/components/pictureinpicture/tests/browser_resistFingerprinting.js ===================================== @@ -0,0 +1,154 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function testResizePip(isRFP, src) { + clearSavedPosition(); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let videoID = "with-controls"; + + let [width, height, pipWidth, pipHeight] = await SpecialPowers.spawn( + browser, + [videoID, src], + async (videoID, src) => { + content.window.resizeEverCalled = false; + let video = content.wrappedJSObject.document.getElementById(videoID); + if (src) { + let { promise, resolve } = Promise.withResolvers(); + video.addEventListener("canplay", resolve); + video.src = src; + await promise; + } + content.document.notifyUserGestureActivation(); + let pip = await video.requestPictureInPicture(); + pip.addEventListener( + "resize", + () => (content.window.resizeEverCalled = true) + ); + return [video.width, video.height, pip.width, pip.height]; + } + ); + info( + `Video size is ${width}x${height}. PiP size is ${pipWidth}x${pipHeight}` + ); + + const { PictureInPicture } = ChromeUtils.importESModule( + "moz-src:///toolkit/components/pictureinpicture/PictureInPicture.sys.mjs" + ); + let pipWindow = PictureInPicture.apiPipWindow?.get(); + Assert.ok(pipWindow, "We found the chrome PiP window."); + + let pipBrowser = pipWindow.document.getElementById("browser"); + await SpecialPowers.spawn(pipBrowser, [], async () => { + let { + promise: setupPromise, + resolve: setupResolve, + reject: setupReject, + } = Promise.withResolvers(); + content.resizePromise = new Promise(resizeResolve => { + let firstObserved = false; + // Scope the observer to the content because we need to block on the + // setup before resizing, therefore we need two separate promises and + // the observer needs to outlive this spawn call. + content.observer = new content.ResizeObserver(() => { + // Attaching the element will trigger a first call to this callback. + // Therefore, we need to ignore it. + if (firstObserved) { + resizeResolve(); + } else { + firstObserved = true; + setupResolve(); + } + }); + let video = content.document.querySelector("video"); + if (video) { + content.observer.observe(video); + } else { + setupReject(new Error("Video not found in the PiP window.")); + } + }); + await setupPromise; + }); + + pipWindow.resizeTo(pipWidth * 2, pipHeight * 2); + await SpecialPowers.spawn(pipBrowser, [], async () => { + await content.resizePromise; + content.observer.disconnect(); + await new Promise(resolve => content.requestAnimationFrame(resolve)); + }); + + let [pipWidthAfter, pipHeightAfter] = await SpecialPowers.spawn( + browser, + [videoID], + async videoID => { + let video = content.wrappedJSObject.document.getElementById(videoID); + // We already have the window, no need to simulate another user + // interaction. + let pip = await video.requestPictureInPicture(); + return [pip.width, pip.height]; + } + ); + info( + `PiP size after resizing ${isRFP ? "with" : "without"} RFP ` + + `is ${pipWidthAfter}x${pipHeightAfter}` + ); + + if (isRFP) { + Assert.equal(pipWidth, pipWidthAfter, "RFP spoofed PiP width."); + Assert.equal(pipHeight, pipHeightAfter, "RFP spoofed PiP height."); + } else { + Assert.notEqual( + pipWidth, + pipWidthAfter, + "After resizing the PiP window, PiP width was updated." + ); + Assert.notEqual( + pipHeight, + pipHeightAfter, + "After resizing the PiP window, PiP height was updated." + ); + } + + let everResized = await SpecialPowers.spawn(browser, [], async () => { + await content.document.exitPictureInPicture(); + return content.window.resizeEverCalled; + }); + Assert.equal( + everResized, + !isRFP, + `We expected the resize handler ${isRFP ? "not " : ""}to be ever called.` + ); + } + ); +} + +add_task(async function test_video_pip_size() { + await testResizePip(false, "test-video.mp4"); +}); + +add_task(async function test_video_pip_size_vertical() { + await testResizePip(false, "test-video-vertical.mp4"); +}); + +add_task(async function test_video_pip_size_rfp() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]], + }); + await testResizePip(true, "test-video.mp4"); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_video_pip_size_rfp_vertical() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]], + }); + await testResizePip(true, "test-video-vertical.mp4"); + await SpecialPowers.popPrefEnv(); +}); View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/3c9945e2... -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/3c9945e2... You're receiving this email because of your account on gitlab.torproject.org. Manage all notifications: https://gitlab.torproject.org/-/profile/notifications | Help: https://gitlab.torproject.org/help