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
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:
| ... | ... | @@ -47,6 +47,11 @@ int32_t PictureInPictureWindow::Width() const { |
| 47 | 47 | if (!IsStateOpened()) {
|
| 48 | 48 | return 0;
|
| 49 | 49 | }
|
| 50 | + RefPtr<HTMLVideoElement> videoElement = mAssociatedVideoElement.get();
|
|
| 51 | + if (videoElement && videoElement->OwnerDoc()->ShouldResistFingerprinting(
|
|
| 52 | + RFPTarget::ScreenRect)) {
|
|
| 53 | + return VideoSizeForRFP().width;
|
|
| 54 | + }
|
|
| 50 | 55 | return mWidth;
|
| 51 | 56 | }
|
| 52 | 57 | |
| ... | ... | @@ -57,14 +62,54 @@ int32_t PictureInPictureWindow::Height() const { |
| 57 | 62 | if (!IsStateOpened()) {
|
| 58 | 63 | return 0;
|
| 59 | 64 | }
|
| 65 | + RefPtr<HTMLVideoElement> videoElement = mAssociatedVideoElement.get();
|
|
| 66 | + if (videoElement && videoElement->OwnerDoc()->ShouldResistFingerprinting(
|
|
| 67 | + RFPTarget::ScreenRect)) {
|
|
| 68 | + return VideoSizeForRFP().height;
|
|
| 69 | + }
|
|
| 60 | 70 | return mHeight;
|
| 61 | 71 | }
|
| 62 | 72 | |
| 73 | +gfx::IntSize PictureInPictureWindow::VideoSizeForRFP() const {
|
|
| 74 | + RefPtr<HTMLVideoElement> videoElement = mAssociatedVideoElement.get();
|
|
| 75 | + if (!videoElement) {
|
|
| 76 | + return {0, 0};
|
|
| 77 | + }
|
|
| 78 | + |
|
| 79 | + // From PictureInPicture.sys.mjs: "The Picture in Picture window will be a
|
|
| 80 | + // maximum of a quarter of the screen height, and a third of the screen
|
|
| 81 | + // width.".
|
|
| 82 | + // Pretend we are maximizing the video in a 1920x1080 display.
|
|
| 83 | + const uint32_t maxWidth = 1920 / 3;
|
|
| 84 | + const uint32_t maxHeight = 1080 / 4;
|
|
| 85 | + uint32_t width = videoElement->VideoWidth();
|
|
| 86 | + uint32_t height = videoElement->VideoHeight();
|
|
| 87 | + if ((height > maxHeight || width > maxWidth) && height > 0) {
|
|
| 88 | + double aspectRatio = static_cast<double>(width) / height;
|
|
| 89 | + if (width >= height) {
|
|
| 90 | + width = maxWidth;
|
|
| 91 | + height = static_cast<uint32_t>(round(maxWidth / aspectRatio));
|
|
| 92 | + } else {
|
|
| 93 | + height = maxHeight;
|
|
| 94 | + width = static_cast<uint32_t>(round(maxHeight * aspectRatio));
|
|
| 95 | + }
|
|
| 96 | + }
|
|
| 97 | + return {width, height};
|
|
| 98 | +}
|
|
| 99 | + |
|
| 63 | 100 | void PictureInPictureWindow::NotifyDimensionsChanged(int32_t aWidth,
|
| 64 | 101 | int32_t aHeight) {
|
| 65 | 102 | mWidth = aWidth;
|
| 66 | 103 | mHeight = aHeight;
|
| 67 | 104 | |
| 105 | + RefPtr<HTMLVideoElement> videoElement = mAssociatedVideoElement.get();
|
|
| 106 | + if (videoElement && videoElement->OwnerDoc()->ShouldResistFingerprinting(
|
|
| 107 | + RFPTarget::ScreenRect)) {
|
|
| 108 | + // With RFP, we spoof the window size to a fixed size that depends on the
|
|
| 109 | + // video, therefore it does not make sense to trigger a resize event.
|
|
| 110 | + return;
|
|
| 111 | + }
|
|
| 112 | + |
|
| 68 | 113 | // When the size of a Picture-in-Picture window pipWindow changes,
|
| 69 | 114 | // the user agent MUST queue a task to fire an event named resize at
|
| 70 | 115 | // pipWindow.
|
| ... | ... | @@ -47,6 +47,8 @@ class PictureInPictureWindow final : public DOMEventTargetHelper { |
| 47 | 47 | private:
|
| 48 | 48 | bool IsStateOpened() const { return mOpened; }
|
| 49 | 49 | |
| 50 | + gfx::IntSize VideoSizeForRFP() const;
|
|
| 51 | + |
|
| 50 | 52 | WeakPtr<HTMLVideoElement> mAssociatedVideoElement;
|
| 51 | 53 | int32_t mWidth = 0;
|
| 52 | 54 | int32_t mHeight = 0;
|
| ... | ... | @@ -315,7 +315,7 @@ bool HTMLVideoElement::IsInteractiveHTMLContent() const { |
| 315 | 315 | HTMLMediaElement::IsInteractiveHTMLContent();
|
| 316 | 316 | }
|
| 317 | 317 | |
| 318 | -gfx::IntSize HTMLVideoElement::GetVideoIntrinsicDimensions() {
|
|
| 318 | +gfx::IntSize HTMLVideoElement::GetVideoIntrinsicDimensions() const {
|
|
| 319 | 319 | const auto& sz = mMediaInfo.mVideo.mDisplay;
|
| 320 | 320 | |
| 321 | 321 | // Prefer the size of the container as it's more up to date.
|
| ... | ... | @@ -324,7 +324,7 @@ gfx::IntSize HTMLVideoElement::GetVideoIntrinsicDimensions() { |
| 324 | 324 | .valueOr(sz);
|
| 325 | 325 | }
|
| 326 | 326 | |
| 327 | -uint32_t HTMLVideoElement::VideoWidth() {
|
|
| 327 | +uint32_t HTMLVideoElement::VideoWidth() const {
|
|
| 328 | 328 | if (!HasVideo()) {
|
| 329 | 329 | return 0;
|
| 330 | 330 | }
|
| ... | ... | @@ -336,7 +336,7 @@ uint32_t HTMLVideoElement::VideoWidth() { |
| 336 | 336 | return size.width;
|
| 337 | 337 | }
|
| 338 | 338 | |
| 339 | -uint32_t HTMLVideoElement::VideoHeight() {
|
|
| 339 | +uint32_t HTMLVideoElement::VideoHeight() const {
|
|
| 340 | 340 | if (!HasVideo()) {
|
| 341 | 341 | return 0;
|
| 342 | 342 | }
|
| ... | ... | @@ -96,9 +96,9 @@ class HTMLVideoElement final : public HTMLMediaElement { |
| 96 | 96 | SetUnsignedIntAttr(nsGkAtoms::height, aValue, 0, aRv);
|
| 97 | 97 | }
|
| 98 | 98 | |
| 99 | - uint32_t VideoWidth();
|
|
| 99 | + uint32_t VideoWidth() const;
|
|
| 100 | 100 | |
| 101 | - uint32_t VideoHeight();
|
|
| 101 | + uint32_t VideoHeight() const;
|
|
| 102 | 102 | |
| 103 | 103 | VideoRotation RotationDegrees() const { return mMediaInfo.mVideo.mRotation; }
|
| 104 | 104 | |
| ... | ... | @@ -179,7 +179,7 @@ class HTMLVideoElement final : public HTMLMediaElement { |
| 179 | 179 | void CreateVideoWakeLockIfNeeded();
|
| 180 | 180 | void ReleaseVideoWakeLockIfExists();
|
| 181 | 181 | |
| 182 | - gfx::IntSize GetVideoIntrinsicDimensions();
|
|
| 182 | + gfx::IntSize GetVideoIntrinsicDimensions() const;
|
|
| 183 | 183 | |
| 184 | 184 | RefPtr<WakeLock> mScreenWakeLock;
|
| 185 | 185 |
| ... | ... | @@ -158,6 +158,8 @@ support-files = ["test-page-with-nan-video-duration.html"] |
| 158 | 158 | |
| 159 | 159 | ["browser_removeVideoElement.js"]
|
| 160 | 160 | |
| 161 | +["browser_resistFingerprinting.js"]
|
|
| 162 | + |
|
| 161 | 163 | ["browser_resizeVideo.js"]
|
| 162 | 164 | skip-if = [
|
| 163 | 165 | "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11'", # Bug 1594223
|
| 1 | +/* Any copyright is dedicated to the Public Domain.
|
|
| 2 | + http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
| 3 | + |
|
| 4 | +"use strict";
|
|
| 5 | + |
|
| 6 | +async function testResizePip(isRFP, src) {
|
|
| 7 | + clearSavedPosition();
|
|
| 8 | + |
|
| 9 | + await BrowserTestUtils.withNewTab(
|
|
| 10 | + {
|
|
| 11 | + url: TEST_PAGE,
|
|
| 12 | + gBrowser,
|
|
| 13 | + },
|
|
| 14 | + async browser => {
|
|
| 15 | + let videoID = "with-controls";
|
|
| 16 | + |
|
| 17 | + let [width, height, pipWidth, pipHeight] = await SpecialPowers.spawn(
|
|
| 18 | + browser,
|
|
| 19 | + [videoID, src],
|
|
| 20 | + async (videoID, src) => {
|
|
| 21 | + content.window.resizeEverCalled = false;
|
|
| 22 | + let video = content.wrappedJSObject.document.getElementById(videoID);
|
|
| 23 | + if (src) {
|
|
| 24 | + let { promise, resolve } = Promise.withResolvers();
|
|
| 25 | + video.addEventListener("canplay", resolve);
|
|
| 26 | + video.src = src;
|
|
| 27 | + await promise;
|
|
| 28 | + }
|
|
| 29 | + content.document.notifyUserGestureActivation();
|
|
| 30 | + let pip = await video.requestPictureInPicture();
|
|
| 31 | + pip.addEventListener(
|
|
| 32 | + "resize",
|
|
| 33 | + () => (content.window.resizeEverCalled = true)
|
|
| 34 | + );
|
|
| 35 | + return [video.width, video.height, pip.width, pip.height];
|
|
| 36 | + }
|
|
| 37 | + );
|
|
| 38 | + info(
|
|
| 39 | + `Video size is ${width}x${height}. PiP size is ${pipWidth}x${pipHeight}`
|
|
| 40 | + );
|
|
| 41 | + |
|
| 42 | + const { PictureInPicture } = ChromeUtils.importESModule(
|
|
| 43 | + "moz-src:///toolkit/components/pictureinpicture/PictureInPicture.sys.mjs"
|
|
| 44 | + );
|
|
| 45 | + let pipWindow = PictureInPicture.apiPipWindow?.get();
|
|
| 46 | + Assert.ok(pipWindow, "We found the chrome PiP window.");
|
|
| 47 | + |
|
| 48 | + let pipBrowser = pipWindow.document.getElementById("browser");
|
|
| 49 | + await SpecialPowers.spawn(pipBrowser, [], async () => {
|
|
| 50 | + let {
|
|
| 51 | + promise: setupPromise,
|
|
| 52 | + resolve: setupResolve,
|
|
| 53 | + reject: setupReject,
|
|
| 54 | + } = Promise.withResolvers();
|
|
| 55 | + content.resizePromise = new Promise(resizeResolve => {
|
|
| 56 | + let firstObserved = false;
|
|
| 57 | + // Scope the observer to the content because we need to block on the
|
|
| 58 | + // setup before resizing, therefore we need two separate promises and
|
|
| 59 | + // the observer needs to outlive this spawn call.
|
|
| 60 | + content.observer = new content.ResizeObserver(() => {
|
|
| 61 | + // Attaching the element will trigger a first call to this callback.
|
|
| 62 | + // Therefore, we need to ignore it.
|
|
| 63 | + if (firstObserved) {
|
|
| 64 | + resizeResolve();
|
|
| 65 | + } else {
|
|
| 66 | + firstObserved = true;
|
|
| 67 | + setupResolve();
|
|
| 68 | + }
|
|
| 69 | + });
|
|
| 70 | + let video = content.document.querySelector("video");
|
|
| 71 | + if (video) {
|
|
| 72 | + content.observer.observe(video);
|
|
| 73 | + } else {
|
|
| 74 | + setupReject(new Error("Video not found in the PiP window."));
|
|
| 75 | + }
|
|
| 76 | + });
|
|
| 77 | + await setupPromise;
|
|
| 78 | + });
|
|
| 79 | + |
|
| 80 | + pipWindow.resizeTo(pipWidth * 2, pipHeight * 2);
|
|
| 81 | + await SpecialPowers.spawn(pipBrowser, [], async () => {
|
|
| 82 | + await content.resizePromise;
|
|
| 83 | + content.observer.disconnect();
|
|
| 84 | + await new Promise(resolve => content.requestAnimationFrame(resolve));
|
|
| 85 | + });
|
|
| 86 | + |
|
| 87 | + let [pipWidthAfter, pipHeightAfter] = await SpecialPowers.spawn(
|
|
| 88 | + browser,
|
|
| 89 | + [videoID],
|
|
| 90 | + async videoID => {
|
|
| 91 | + let video = content.wrappedJSObject.document.getElementById(videoID);
|
|
| 92 | + // We already have the window, no need to simulate another user
|
|
| 93 | + // interaction.
|
|
| 94 | + let pip = await video.requestPictureInPicture();
|
|
| 95 | + return [pip.width, pip.height];
|
|
| 96 | + }
|
|
| 97 | + );
|
|
| 98 | + info(
|
|
| 99 | + `PiP size after resizing ${isRFP ? "with" : "without"} RFP ` +
|
|
| 100 | + `is ${pipWidthAfter}x${pipHeightAfter}`
|
|
| 101 | + );
|
|
| 102 | + |
|
| 103 | + if (isRFP) {
|
|
| 104 | + Assert.equal(pipWidth, pipWidthAfter, "RFP spoofed PiP width.");
|
|
| 105 | + Assert.equal(pipHeight, pipHeightAfter, "RFP spoofed PiP height.");
|
|
| 106 | + } else {
|
|
| 107 | + Assert.notEqual(
|
|
| 108 | + pipWidth,
|
|
| 109 | + pipWidthAfter,
|
|
| 110 | + "After resizing the PiP window, PiP width was updated."
|
|
| 111 | + );
|
|
| 112 | + Assert.notEqual(
|
|
| 113 | + pipHeight,
|
|
| 114 | + pipHeightAfter,
|
|
| 115 | + "After resizing the PiP window, PiP height was updated."
|
|
| 116 | + );
|
|
| 117 | + }
|
|
| 118 | + |
|
| 119 | + let everResized = await SpecialPowers.spawn(browser, [], async () => {
|
|
| 120 | + await content.document.exitPictureInPicture();
|
|
| 121 | + return content.window.resizeEverCalled;
|
|
| 122 | + });
|
|
| 123 | + Assert.equal(
|
|
| 124 | + everResized,
|
|
| 125 | + !isRFP,
|
|
| 126 | + `We expected the resize handler ${isRFP ? "not " : ""}to be ever called.`
|
|
| 127 | + );
|
|
| 128 | + }
|
|
| 129 | + );
|
|
| 130 | +}
|
|
| 131 | + |
|
| 132 | +add_task(async function test_video_pip_size() {
|
|
| 133 | + await testResizePip(false, "test-video.mp4");
|
|
| 134 | +});
|
|
| 135 | + |
|
| 136 | +add_task(async function test_video_pip_size_vertical() {
|
|
| 137 | + await testResizePip(false, "test-video-vertical.mp4");
|
|
| 138 | +});
|
|
| 139 | + |
|
| 140 | +add_task(async function test_video_pip_size_rfp() {
|
|
| 141 | + await SpecialPowers.pushPrefEnv({
|
|
| 142 | + set: [["privacy.resistFingerprinting", true]],
|
|
| 143 | + });
|
|
| 144 | + await testResizePip(true, "test-video.mp4");
|
|
| 145 | + await SpecialPowers.popPrefEnv();
|
|
| 146 | +});
|
|
| 147 | + |
|
| 148 | +add_task(async function test_video_pip_size_rfp_vertical() {
|
|
| 149 | + await SpecialPowers.pushPrefEnv({
|
|
| 150 | + set: [["privacy.resistFingerprinting", true]],
|
|
| 151 | + });
|
|
| 152 | + await testResizePip(true, "test-video-vertical.mp4");
|
|
| 153 | + await SpecialPowers.popPrefEnv();
|
|
| 154 | +}); |