File: DocumentPictureInPicture.cpp

package info (click to toggle)
firefox 148.0.2-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 4,719,544 kB
  • sloc: cpp: 7,618,291; javascript: 6,701,749; ansic: 3,781,787; python: 1,418,389; xml: 638,647; asm: 438,962; java: 186,285; sh: 62,894; makefile: 19,011; objc: 13,092; perl: 12,763; yacc: 4,583; cs: 3,846; pascal: 3,448; lex: 1,720; ruby: 1,003; php: 436; lisp: 258; awk: 247; sql: 66; sed: 54; csh: 10; exp: 6
file content (362 lines) | stat: -rw-r--r-- 13,612 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

#include "mozilla/dom/DocumentPictureInPicture.h"

#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/WidgetUtils.h"
#include "mozilla/dom/BrowserChild.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/DocumentPictureInPictureEvent.h"
#include "mozilla/dom/WindowContext.h"
#include "mozilla/widget/Screen.h"
#include "nsDocShell.h"
#include "nsDocShellLoadState.h"
#include "nsIWindowWatcher.h"
#include "nsNetUtil.h"
#include "nsPIWindowWatcher.h"
#include "nsServiceManagerUtils.h"
#include "nsWindowWatcher.h"

namespace mozilla::dom {

static mozilla::LazyLogModule gDPIPLog("DocumentPIP");

NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentPictureInPicture)

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentPictureInPicture,
                                                  DOMEventTargetHelper)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLastOpenedWindow)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentPictureInPicture,
                                                DOMEventTargetHelper)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mLastOpenedWindow)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentPictureInPicture)
  NS_INTERFACE_MAP_ENTRY(nsIObserver)
  NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)

NS_IMPL_ADDREF_INHERITED(DocumentPictureInPicture, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(DocumentPictureInPicture, DOMEventTargetHelper)

JSObject* DocumentPictureInPicture::WrapObject(
    JSContext* cx, JS::Handle<JSObject*> aGivenProto) {
  return DocumentPictureInPicture_Binding::Wrap(cx, this, aGivenProto);
}

DocumentPictureInPicture::DocumentPictureInPicture(nsPIDOMWindowInner* aWindow)
    : DOMEventTargetHelper(aWindow) {
  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
  NS_ENSURE_TRUE_VOID(os);
  DebugOnly<nsresult> rv = os->AddObserver(this, "domwindowclosed", false);
  MOZ_ASSERT(NS_SUCCEEDED(rv));
}

DocumentPictureInPicture::~DocumentPictureInPicture() {
  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
  NS_ENSURE_TRUE_VOID(os);
  DebugOnly<nsresult> rv = os->RemoveObserver(this, "domwindowclosed");
  MOZ_ASSERT(NS_SUCCEEDED(rv));
}

void DocumentPictureInPicture::OnPiPResized() {
  if (!mLastOpenedWindow) {
    return;
  }

  RefPtr<nsGlobalWindowInner> innerWindow =
      nsGlobalWindowInner::Cast(mLastOpenedWindow);

  int x = innerWindow->GetScreenLeft(CallerType::System, IgnoreErrors());
  int y = innerWindow->GetScreenTop(CallerType::System, IgnoreErrors());
  int width = static_cast<int>(innerWindow->GetInnerWidth(IgnoreErrors()));
  int height = static_cast<int>(innerWindow->GetInnerHeight(IgnoreErrors()));

  mPreviousExtent = Some(CSSIntRect(x, y, width, height));

  MOZ_LOG(gDPIPLog, LogLevel::Debug,
          ("PiP was resized, remembering position %s",
           ToString(mPreviousExtent).c_str()));
}

void DocumentPictureInPicture::OnPiPClosed() {
  if (!mLastOpenedWindow) {
    return;
  }

  RefPtr<nsGlobalWindowInner> pipInnerWindow =
      nsGlobalWindowInner::Cast(mLastOpenedWindow);
  pipInnerWindow->RemoveSystemEventListener(u"resize"_ns, this, true);

  MOZ_LOG(gDPIPLog, LogLevel::Debug, ("PiP was closed"));

  mLastOpenedWindow = nullptr;
}

nsGlobalWindowInner* DocumentPictureInPicture::GetWindow() {
  if (mLastOpenedWindow && mLastOpenedWindow->GetOuterWindow() &&
      !mLastOpenedWindow->GetOuterWindow()->Closed()) {
    return nsGlobalWindowInner::Cast(mLastOpenedWindow);
  }
  return nullptr;
}

// Some sane default. Maybe we should come up with an heuristic based on screen
// size.
const CSSIntSize DocumentPictureInPicture::sDefaultSize = {700, 650};
const CSSIntSize DocumentPictureInPicture::sMinSize = {240, 50};

static nsresult OpenPiPWindowUtility(nsPIDOMWindowOuter* aParent,
                                     const CSSIntRect& aExtent, bool aPrivate,
                                     mozilla::dom::BrowsingContext** aRet) {
  MOZ_DIAGNOSTIC_ASSERT(aParent);

  nsresult rv = NS_OK;
  nsCOMPtr<nsIWindowWatcher> ww =
      do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsPIWindowWatcher> pww(do_QueryInterface(ww));
  NS_ENSURE_TRUE(pww, NS_ERROR_FAILURE);

  nsCOMPtr<nsIURI> uri;
  rv = NS_NewURI(getter_AddRefs(uri), "about:blank"_ns, nullptr);
  NS_ENSURE_SUCCESS(rv, rv);

  RefPtr<nsDocShellLoadState> loadState =
      nsWindowWatcher::CreateLoadState(uri, aParent);

  // pictureinpicture is a non-standard window feature not available from JS
  nsPrintfCString features("pictureinpicture,top=%d,left=%d,width=%d,height=%d",
                           aExtent.y, aExtent.x, aExtent.width, aExtent.height);

  rv = pww->OpenWindow2(aParent, uri, "_blank"_ns, features,
                        mozilla::dom::UserActivation::Modifiers::None(), false,
                        false, true, nullptr, false, false, false,
                        nsPIWindowWatcher::PrintKind::PRINT_NONE, loadState,
                        aRet);
  NS_ENSURE_SUCCESS(rv, rv);
  NS_ENSURE_TRUE(aRet, NS_ERROR_FAILURE);
  return NS_OK;
}

/* static */
Maybe<CSSIntRect> DocumentPictureInPicture::GetScreenRect(
    nsPIDOMWindowOuter* aWindow) {
  nsCOMPtr<nsIWidget> widget = widget::WidgetUtils::DOMWindowToWidget(aWindow);
  NS_ENSURE_TRUE(widget, Nothing());
  RefPtr<widget::Screen> screen = widget->GetWidgetScreen();
  NS_ENSURE_TRUE(screen, Nothing());
  LayoutDeviceIntRect rect = screen->GetRect();

  nsGlobalWindowOuter* outerWindow = nsGlobalWindowOuter::Cast(aWindow);
  NS_ENSURE_TRUE(outerWindow, Nothing());
  nsCOMPtr<nsIBaseWindow> treeOwnerAsWin = outerWindow->GetTreeOwnerWindow();
  NS_ENSURE_TRUE(treeOwnerAsWin, Nothing());
  auto scale = outerWindow->CSSToDevScaleForBaseWindow(treeOwnerAsWin);

  return Some(RoundedToInt(rect / scale));
}

// Place window in the bottom right of the opener window's screen
static CSSIntPoint CalcInitialPos(const CSSIntRect& screen,
                                  const CSSIntSize& aSize) {
  // aSize is the inner size not including browser UI. But we need the outer
  // size for calculating where the top left corner of the PiP should be
  // initially. For now use a guess of ~80px for the browser UI?
  return {std::max(screen.X(), screen.XMost() - aSize.width - 100),
          std::max(screen.Y(), screen.YMost() - aSize.height - 100 - 80)};
}

/* static */
CSSIntSize DocumentPictureInPicture::CalcMaxDimensions(
    const CSSIntRect& screen) {
  // Limit PIP size to 80% (arbitrary number) of screen size
  // https://wicg.github.io/document-picture-in-picture/#maximum-size
  CSSIntSize size =
      RoundedToInt(screen.Size() * gfx::ScaleFactor<CSSPixel, CSSPixel>(0.8));
  size.width = std::max(size.width, sMinSize.width);
  size.height = std::max(size.height, sMinSize.height);
  return size;
}

CSSIntRect DocumentPictureInPicture::DetermineExtent(
    bool aPreferInitialWindowPlacement, int aRequestedWidth,
    int aRequestedHeight, const CSSIntRect& screen) {
  // If we remembered an extent, don't preferInitialWindowPlacement, and the
  // requested size didn't change, then restore the remembered extent.
  const bool shouldUseInitialPlacement =
      !mPreviousExtent.isSome() || aPreferInitialWindowPlacement ||
      (mLastRequestedSize.isSome() &&
       (mLastRequestedSize->Width() != aRequestedWidth ||
        mLastRequestedSize->Height() != aRequestedHeight));

  CSSIntRect extent;
  if (shouldUseInitialPlacement) {
    CSSIntSize size = sDefaultSize;
    if (aRequestedWidth > 0 && aRequestedHeight > 0) {
      size = CSSIntSize(aRequestedWidth, aRequestedHeight);
    }
    CSSIntPoint initialPos = CalcInitialPos(screen, size);
    extent = CSSIntRect(initialPos, size);

    MOZ_LOG(gDPIPLog, LogLevel::Debug,
            ("Calculated initial PiP rect %s", ToString(extent).c_str()));
  } else {
    extent = mPreviousExtent.value();
  }

  // https://wicg.github.io/document-picture-in-picture/#maximum-size
  CSSIntSize maxSize = CalcMaxDimensions(screen);
  extent.width = std::clamp(extent.width, sMinSize.width, maxSize.width);
  extent.height = std::clamp(extent.height, sMinSize.height, maxSize.height);

  return extent;
}

already_AddRefed<Promise> DocumentPictureInPicture::RequestWindow(
    const DocumentPictureInPictureOptions& aOptions, ErrorResult& aRv) {
  // Not part of the spec, but check the document is active
  RefPtr<nsPIDOMWindowInner> ownerWin = GetOwnerWindow();
  if (!ownerWin || !ownerWin->IsFullyActive()) {
    aRv.ThrowNotAllowedError("Document is not fully active");
    return nullptr;
  }

  // 2. Throw if not top-level
  BrowsingContext* bc = ownerWin->GetBrowsingContext();
  if (!bc || !bc->IsTop()) {
    aRv.ThrowNotAllowedError(
        "Document Picture-in-Picture is only available in top-level contexts");
    return nullptr;
  }

  // 3. Throw if already in a Document PIP window
  if (bc->GetIsDocumentPiP()) {
    aRv.ThrowNotAllowedError(
        "Cannot open a Picture-in-Picture window from inside one");
    return nullptr;
  }

  // 4, 7. Require transient activation
  WindowContext* wc = ownerWin->GetWindowContext();
  if (!wc || !wc->ConsumeTransientUserGestureActivation()) {
    aRv.ThrowNotAllowedError(
        "Document Picture-in-Picture requires user activation");
    return nullptr;
  }

  // 5-6. If width or height is given, both must be specified
  if ((aOptions.mWidth > 0) != (aOptions.mHeight > 0)) {
    aRv.ThrowRangeError(
        "requestWindow: width and height must be specified together");
    return nullptr;
  }

  // 8. Possibly close last opened window
  if (RefPtr<nsPIDOMWindowInner> lastOpenedWindow = mLastOpenedWindow) {
    lastOpenedWindow->Close();
  }

  CSSIntRect screen;
  if (Maybe<CSSIntRect> maybeScreen =
          GetScreenRect(ownerWin->GetOuterWindow())) {
    screen = maybeScreen.value();
  } else {
    aRv.ThrowRangeError("Could not determine screen for window");
    return nullptr;
  }

  // 13-15. Determine PiP extent
  const int requestedWidth = SaturatingCast<int>(aOptions.mWidth),
            requestedHeight = SaturatingCast<int>(aOptions.mHeight);
  CSSIntRect extent = DetermineExtent(aOptions.mPreferInitialWindowPlacement,
                                      requestedWidth, requestedHeight, screen);
  mLastRequestedSize = Some(CSSIntSize(requestedWidth, requestedHeight));

  MOZ_LOG(gDPIPLog, LogLevel::Debug,
          ("Will place PiP at rect %s", ToString(extent).c_str()));

  // 9. Optionally, close any existing PIP windows
  // I think it's useful to have multiple PiP windows from different top pages.

  // 15. aOptions.mDisallowReturnToOpener
  // I think this button is redundant with close and the webpage won't know
  // whether close or return was pressed. So let's not have that button at all.

  // 10. Create a new top-level traversable for target _blank
  // 16. Configure PIP to float on top via window features
  RefPtr<BrowsingContext> pipTraversable;
  nsresult rv = OpenPiPWindowUtility(ownerWin->GetOuterWindow(), extent,
                                     bc->UsePrivateBrowsing(),
                                     getter_AddRefs(pipTraversable));
  if (NS_FAILED(rv)) {
    aRv.ThrowUnknownError("Failed to create PIP window");
    return nullptr;
  }

  // 11. Set PIP's active document's mode to this's document's mode
  pipTraversable->GetDocument()->SetCompatibilityMode(
      ownerWin->GetDoc()->GetCompatibilityMode());

  // 12. Set PIP's IsDocumentPIP flag
  rv = pipTraversable->SetIsDocumentPiP(true);
  MOZ_ASSERT(NS_SUCCEEDED(rv));

  // 16. Set mLastOpenedWindow
  mLastOpenedWindow = pipTraversable->GetDOMWindow()->GetCurrentInnerWindow();
  MOZ_ASSERT(mLastOpenedWindow);

  // Keep track of resizes to update mPreviousExtent
  RefPtr<nsGlobalWindowInner> pipInnerWindow =
      nsGlobalWindowInner::Cast(mLastOpenedWindow);
  pipInnerWindow->AddSystemEventListener(u"resize"_ns, this, true, false);

  // 17. Queue a task to fire a DocumentPictureInPictureEvent named "enter" on
  // this with pipTraversable as it's window attribute
  DocumentPictureInPictureEventInit eventInit;
  eventInit.mWindow = pipInnerWindow;
  RefPtr<Event> event =
      DocumentPictureInPictureEvent::Constructor(this, u"enter"_ns, eventInit);
  RefPtr<AsyncEventDispatcher> asyncDispatcher =
      new AsyncEventDispatcher(this, event.forget());
  asyncDispatcher->PostDOMEvent();

  // 18. Return pipTraversable
  RefPtr<Promise> promise = Promise::CreateInfallible(GetOwnerGlobal());
  promise->MaybeResolve(pipInnerWindow);
  return promise.forget();
}

NS_IMETHODIMP
DocumentPictureInPicture::HandleEvent(Event* aEvent) {
  nsAutoString type;
  aEvent->GetType(type);

  if (type.EqualsLiteral("resize")) {
    OnPiPResized();
    return NS_OK;
  }

  return NS_OK;
}

NS_IMETHODIMP DocumentPictureInPicture::Observe(nsISupports* aSubject,
                                                const char* aTopic,
                                                const char16_t* aData) {
  if (nsCRT::strcmp(aTopic, "domwindowclosed") == 0) {
    nsCOMPtr<nsPIDOMWindowOuter> subjectWin = do_QueryInterface(aSubject);
    NS_ENSURE_TRUE(!!subjectWin, NS_OK);

    if (subjectWin->GetCurrentInnerWindow() == mLastOpenedWindow) {
      OnPiPClosed();
    }
  }
  return NS_OK;
}

}  // namespace mozilla::dom