Source code
Revision control
Copy as Markdown
Other Tools
/* 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
#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
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();
}
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