Files
sousa-gecko/dom/media/PictureInPictureService.cpp
T
Simon Farre 5107077c7d Bug 1463402: Implement Picture In Picture API r=dom-core,webidl,emilio,smaug,devtools-reviewers,ochameau
Implements the gecko-parts of Picture In Picture API, to which the various platforms can add their implementation for opening a PIP Window to.

nsIPictureInPictureFunctions.idl shall be implemented by the different platforms. For desktop we already have a "native" PIP implementation and this will be re-used.

Platforms which does not implement nsIPictureInPictureFunctions.idl, will signal that they don't support it (and attempts to request via the API algorithms, will reject accordingly).

Differential Revision: https://phabricator.services.mozilla.com/D286141
2026-05-29 10:10:44 +00:00

303 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 http://mozilla.org/MPL/2.0/. */
#include "PictureInPictureService.h"
#include "PictureInPictureWindow.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/HTMLVideoElement.h"
#include "mozilla/dom/PictureInPictureWindow.h"
#include "nsComponentManagerUtils.h"
namespace mozilla::dom {
StaticRefPtr<PictureInPictureService> gPictureInPictureService;
PictureInPictureRequest::~PictureInPictureRequest() = default;
NS_IMPL_CYCLE_COLLECTION(PictureInPictureRequest, mVideo, mPromise,
mPictureInPictureWindowInstance)
NS_IMPL_CYCLE_COLLECTING_ADDREF(PictureInPictureRequest)
NS_IMPL_CYCLE_COLLECTING_RELEASE(PictureInPictureRequest)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PictureInPictureRequest)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
void PictureInPictureService::EnsureInit() {
MOZ_ASSERT(NS_IsMainThread());
if (!gPictureInPictureService) [[unlikely]] {
gPictureInPictureService = new PictureInPictureService();
ClearOnShutdown(&gPictureInPictureService, ShutdownPhase::XPCOMShutdown);
if (!gPictureInPictureService->InitializeProvider()) {
NS_WARNING(
"Video Picture-in-Picture not yet supported on this platform.");
}
}
}
// Platforms that do not implement PictureInPictureFunctions have video
// PictureInPicture disabled.
bool PictureInPictureService::IsSupported() {
EnsureInit();
return gPictureInPictureService->mPictureInPictureProvider;
}
/* static */
void PictureInPictureService::OpenPictureInPictureWindow(
Promise* aPromise, HTMLVideoElement* aVideo) {
const Document* doc = aVideo->OwnerDoc();
nsPIDOMWindowInner* window = doc->GetInnerWindow();
if (!window) {
aPromise->MaybeRejectWithInvalidStateError("No document or window");
return;
}
// 10.1. If this is pictureInPictureElement:
if (doc->GetPictureInPictureElementInternal() == aVideo) {
// 1. Queue a global task on the media element event task source given
// global to resolve p with the Picture-in-Picture window associated
// with pictureInPictureElement
// 2. abort these steps
// N.B. Function is called from within dispatched runnable. No queuing
// necessary.
MOZ_ASSERT(aVideo->GetAssociatedPictureInPictureWindow());
aPromise->MaybeResolve(aVideo->GetAssociatedPictureInPictureWindow());
return;
}
// Stash the PIP Window instance for later use as well as we need to be
// able to provide it to the picture in picture functions (see
// nsIMediaPictureInPictureProvider.idl)
RefPtr<PictureInPictureWindow> pipWindowInstance =
MakeRefPtr<PictureInPictureWindow>(window, aVideo);
// 10.2 Attempt to associate a Picture-in-Picture window with this.
RefPtr<Promise> servicePromise =
PictureInPictureService::AssociatePictureInPictureWindowWith(
aVideo, pipWindowInstance);
// Note: if the promise is null here, it's technically not 10.3. 10.3 actually
// happens in the rejection case of the service promise, which executes at a
// later time. 10.3 If the previous step failed:
if (!servicePromise) {
// 1. Queue a global task on the media element event task source given
// global to reject p with InvalidStateError DOMException.
// N.B. Function is called from within dispatched runnable. No queuing
// necessary.
aPromise->MaybeRejectWithInvalidStateError("Failed to create PIP Window");
// 2. Abort these steps.
return;
}
auto request = MakeRefPtr<EnterPictureInPictureRequest>(aPromise, aVideo,
pipWindowInstance);
// Remainder of parallel steps happen when 10.2 completes
servicePromise->AppendNativeHandler(request);
}
/* static */
void PictureInPictureService::DispatchExitPictureInPictureRunnable(
Promise* aPromise, HTMLVideoElement* aVideo) {
NS_DispatchToMainThread(NS_NewRunnableFunction(
__func__, [promise = RefPtr{aPromise},
video = RefPtr{aVideo}]() MOZ_CAN_RUN_SCRIPT_BOUNDARY {
PictureInPictureService::ExitPictureInPictureWindow(promise, video);
}));
}
/* static */
void PictureInPictureService::ExitPictureInPictureWindow(
Promise* aPromise, HTMLVideoElement* aVideo) {
Document* doc = aVideo->OwnerDoc();
// Note: Spec needs update for this check, due to it's in-parallel
// nature.
Element* pictureInPictureElement = doc->GetPictureInPictureElementInternal();
// if pipElement != mVideo, means exit already exited for mVideo (or
// something like it)
if (!pictureInPictureElement || pictureInPictureElement != aVideo) {
if (aPromise) {
aPromise->MaybeResolveWithUndefined();
}
return;
}
// 2. Run the close window algorithm with the Picture-in-Picture window
// associated with pictureInPictureElement.
RefPtr<Promise> servicePromise =
PictureInPictureService::ClosePictureInPictureWindow(aVideo);
// Note: PR for requestPictureInPicture has language like "if user agent
// deems necessary, it can abort by rejecting with invalid state error".
if (!servicePromise) {
if (aPromise) {
aPromise->MaybeRejectWithInvalidStateError(
"Failed to create exit picture in picture request.");
}
return;
}
auto request = MakeRefPtr<ExitPictureInPictureRequest>(aPromise, aVideo);
// Remainder of exit steps happen in
// HTMLVideoElement::EndCloningVisually as this can be called via web
// content JS, but also via the "native to Firefox" PIP implementation.
servicePromise->AppendNativeHandler(request);
}
/* static */
RefPtr<Promise> PictureInPictureService::AssociatePictureInPictureWindowWith(
HTMLVideoElement* aElement, PictureInPictureWindow* aWindow) {
if (!IsSupported()) {
return nullptr;
}
AutoJSAPI jsapi;
if (NS_WARN_IF(!jsapi.Init(aElement->GetRelevantGlobal()))) {
return nullptr;
}
JSContext* cx = jsapi.cx();
RefPtr<Promise> chromePromise;
nsresult rv = gPictureInPictureService->mPictureInPictureProvider
->OpenMediaPictureInPictureWindow(
aElement, aWindow, cx, getter_AddRefs(chromePromise));
if (NS_WARN_IF(NS_FAILED(rv) || !chromePromise)) {
return nullptr;
}
return chromePromise;
}
/* static */
RefPtr<Promise> PictureInPictureService::ClosePictureInPictureWindow(
Element* aElement) {
if (!IsSupported()) {
return nullptr;
}
AutoJSAPI jsapi;
nsPIDOMWindowInner* window = aElement->OwnerDoc()->GetInnerWindow();
if (NS_WARN_IF(!jsapi.Init(window))) {
return nullptr;
}
JSContext* cx = jsapi.cx();
RefPtr<Promise> chromePromise;
nsresult rv = gPictureInPictureService->mPictureInPictureProvider
->CloseMediaPictureInPictureWindow(
aElement, cx, getter_AddRefs(chromePromise));
if (NS_WARN_IF(NS_FAILED(rv) || !chromePromise)) {
return nullptr;
}
return chromePromise;
}
bool PictureInPictureService::InitializeProvider() {
if (!mPictureInPictureProvider) {
mPictureInPictureProvider =
do_CreateInstance("@mozilla.org/toolkit/picture-in-picture-provider");
if (NS_WARN_IF(!mPictureInPictureProvider)) {
return false;
}
}
return true;
}
PictureInPictureRequest::PictureInPictureRequest(
Promise* aPromise, HTMLVideoElement* aVideo,
PictureInPictureWindow* aPipWindow)
: mPictureInPictureWindowInstance(aPipWindow),
mVideo(aVideo),
mPromise(aPromise) {}
void PictureInPictureRequest::ResolvedCallback(JSContext* aCx,
JS::Handle<JS::Value> aValue,
ErrorResult& aRv) {
MOZ_ASSERT(NS_IsMainThread());
OnServicePromiseSettled(true);
}
void PictureInPictureRequest::RejectedCallback(JSContext* aCx,
JS::Handle<JS::Value> aValue,
ErrorResult& aRv) {
MOZ_ASSERT(NS_IsMainThread());
OnServicePromiseSettled(false);
}
EnterPictureInPictureRequest::EnterPictureInPictureRequest(
Promise* aPromise, HTMLVideoElement* aVideo,
PictureInPictureWindow* aPipWindow)
: PictureInPictureRequest(aPromise, aVideo, aPipWindow) {}
void EnterPictureInPictureRequest::OnServicePromiseSettled(bool aResolved) {
// Note: We run when the "service promise" is settled and we should safely be
// able to run script here, no queueing required.
// 10.5 Queue a global task on the media element event task source given
// global, to perform the following steps:
Document* doc = mVideo->OwnerDoc();
if (aResolved && mPromise) {
// 1. Set pictureInPictureElement to this.
doc->SetPictureInPictureElement(mVideo);
// TODO: Maybe implement? I don't see the use case for it.
// 2. Append relevant settings object’s origin to initiators of active
// Picture-in-Picture sessions.
mVideo->AddStates(ElementState::PICTURE_IN_PICTURE);
mVideo->SetAssociatedPictureInPictureWindow(
mPictureInPictureWindowInstance);
// 3. If pictureInPictureElement is fullscreenElement, it is RECOMMENDED
// to exit fullscreen.
if (mVideo == doc->GetUnretargetedFullscreenElement()) {
Document::AsyncExitFullscreen(doc);
}
// 4. Fire an event named enterpictureinpicture using
// PictureInPictureEvent at the video with its bubbles attribute initialized
// to true and its pictureInPictureWindow attribute initialized to
// Picture-in-Picture window.
PictureInPictureEventInit eventInit;
eventInit.mBubbles = true;
eventInit.mCancelable = false;
eventInit.mPictureInPictureWindow = mPictureInPictureWindowInstance;
RefPtr<PictureInPictureEvent> pipEvent = PictureInPictureEvent::Constructor(
mVideo, u"enterpictureinpicture"_ns, eventInit);
pipEvent->SetTrusted(true);
mVideo->DispatchEvent(*pipEvent);
// 5. Resolve p with the Picture-in-Picture window associated with
// pictureInPictureElement.
mPromise->MaybeResolve(mPictureInPictureWindowInstance);
} else if (!aResolved && mPromise) {
// This is basically also 10.3.1 the failure step.
mPromise->MaybeRejectWithInvalidStateError(
"Picture-in-Picture request failed");
}
}
ExitPictureInPictureRequest::ExitPictureInPictureRequest(
Promise* aPromise, HTMLVideoElement* aVideo)
: PictureInPictureRequest(aPromise, aVideo,
aVideo->GetAssociatedPictureInPictureWindow()) {}
void ExitPictureInPictureRequest::OnServicePromiseSettled(bool aResolved) {
// If we don't have a promise here, something requested exit but not via
// document.exitPictureInPicture()
if (!mPromise) {
return;
}
if (!aResolved) {
mPromise->MaybeRejectWithInvalidStateError("PiP request failed");
return;
}
mPromise->MaybeResolveWithUndefined();
}
} // namespace mozilla::dom