morgan pushed to branch tor-browser-153.1.0esr-16.0-1 at The Tor Project / Applications / Tor Browser

Commits:

6 changed files:

Changes:

  • dom/media/PictureInPictureWindow.cpp
    ... ... @@ -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.
    

  • dom/media/PictureInPictureWindow.h
    ... ... @@ -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;
    

  • dom/media/mediaelement/HTMLVideoElement.cpp
    ... ... @@ -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
       }
    

  • dom/media/mediaelement/HTMLVideoElement.h
    ... ... @@ -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
     
    

  • toolkit/components/pictureinpicture/tests/browser.toml
    ... ... @@ -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
    

  • toolkit/components/pictureinpicture/tests/browser_resistFingerprinting.js
    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
    +});