Bug 1739220 - Handle fullscreen state in a more reliable way; r=smaug,Gijs

Differential Revision: https://phabricator.services.mozilla.com/D131185
This commit is contained in:
Edgar Chen 2021-12-17 09:15:10 +00:00
parent 73230b8c3e
commit f9a4a6bd64
3 changed files with 174 additions and 61 deletions

View File

@ -11,26 +11,24 @@ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
class DOMFullscreenChild extends JSWindowActorChild {
receiveMessage(aMessage) {
let window = this.contentWindow;
if (!window) {
if (!aMessage.data.remoteFrameBC) {
this.sendAsyncMessage("DOMFullscreen:Exit", {});
}
return;
}
let windowUtils = window.windowUtils;
if (!windowUtils) {
return;
}
let windowUtils = window?.windowUtils;
switch (aMessage.name) {
case "DOMFullscreen:Entered": {
if (!windowUtils) {
// If we are not able to enter fullscreen, tell the parent to just
// exit.
this.sendAsyncMessage("DOMFullscreen:Exit", {});
break;
}
let remoteFrameBC = aMessage.data.remoteFrameBC;
if (remoteFrameBC) {
let remoteFrame = remoteFrameBC.embedderElement;
this._isNotTheRequestSource = true;
windowUtils.remoteFrameFullscreenChanged(remoteFrame);
} else {
this._waitForMozAfterPaint = true;
this._lastTransactionId = windowUtils.lastTransactionId;
if (
!windowUtils.handleFullscreenRequests() &&
@ -45,23 +43,39 @@ class DOMFullscreenChild extends JSWindowActorChild {
break;
}
case "DOMFullscreen:CleanUp": {
let remoteFrameBC = aMessage.data.remoteFrameBC;
if (remoteFrameBC) {
this._isNotTheRequestSource = true;
}
let isNotTheRequestSource = !!aMessage.data.remoteFrameBC;
// If we've exited fullscreen at this point, no need to record
// transaction id or call exit fullscreen. This is especially
// important for pre-e10s, since in that case, it is possible
// that no more paint would be triggered after this point.
if (this.document.fullscreenElement) {
this._lastTransactionId = windowUtils.lastTransactionId;
windowUtils.exitFullscreen();
this._isNotTheRequestSource = isNotTheRequestSource;
// Need to wait for the MozAfterPaint after exiting fullscreen if
// this is the request source.
this._waitForMozAfterPaint = !this._isNotTheRequestSource;
// windowUtils could be null if the associated window is not current
// active window. In this case, document must be in the process of
// exiting fullscreen, it is okay to not ask it to exit fullscreen.
if (windowUtils) {
this._lastTransactionId = windowUtils.lastTransactionId;
windowUtils.exitFullscreen();
}
} else if (isNotTheRequestSource) {
// If we are not the request source and have exited fullscreen, reply
// Exited to parent as parent is waiting for our reply.
this.sendAsyncMessage("DOMFullscreen:Exited", {});
} else {
// If we've already exited fullscreen, it is possible that no more
// paint would be triggered, so don't wait for MozAfterPaint.
// TODO: There might be some way to move this code around a bit to
// make it easier to follow. Somehow handle the "local" case in
// one place and the isNotTheRequestSource case after that.
this.sendAsyncMessage("DOMFullscreen:Painted", {});
}
break;
}
case "DOMFullscreen:Painted": {
Services.obs.notifyObservers(this.contentWindow, "fullscreen-painted");
Services.obs.notifyObservers(window, "fullscreen-painted");
break;
}
}
@ -99,15 +113,20 @@ class DOMFullscreenChild extends JSWindowActorChild {
delete this._isNotTheRequestSource;
this.sendAsyncMessage(aEvent.type.replace("Moz", ""), {});
} else {
let rootWindow = this.contentWindow.windowRoot;
rootWindow.addEventListener("MozAfterPaint", this);
if (!this.document || !this.document.fullscreenElement) {
// If we receive any fullscreen change event, and find we are
// actually not in fullscreen, also ask the parent to exit to
// ensure that the parent always exits fullscreen when we do.
this.sendAsyncMessage("DOMFullscreen:Exit", {});
}
break;
}
if (this._waitForMozAfterPaint) {
delete this._waitForMozAfterPaint;
this._listeningWindow = this.contentWindow.windowRoot;
this._listeningWindow.addEventListener("MozAfterPaint", this);
}
if (!this.document || !this.document.fullscreenElement) {
// If we receive any fullscreen change event, and find we are
// actually not in fullscreen, also ask the parent to exit to
// ensure that the parent always exits fullscreen when we do.
this.sendAsyncMessage("DOMFullscreen:Exit", {});
}
break;
}
@ -120,8 +139,8 @@ class DOMFullscreenChild extends JSWindowActorChild {
!this._lastTransactionId ||
aEvent.transactionId > this._lastTransactionId
) {
let rootWindow = this.contentWindow.windowRoot;
rootWindow.removeEventListener("MozAfterPaint", this);
this._listeningWindow.removeEventListener("MozAfterPaint", this);
delete this._listeningWindow;
this.sendAsyncMessage("DOMFullscreen:Painted", {});
}
break;

View File

@ -9,7 +9,19 @@ var EXPORTED_SYMBOLS = ["DOMFullscreenParent"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
class DOMFullscreenParent extends JSWindowActorParent {
waitingForChildFullscreen = false;
// These properties get set by browser-fullScreenAndPointerLock.js.
// TODO: Bug 1743703 - Consider moving the messaging component of
// browser-fullScreenAndPointerLock.js into the actor
waitingForChildEnterFullscreen = false;
waitingForChildExitFullscreen = false;
// Cache the next message recipient actor and in-process browsing context that
// is computed by _getNextMsgRecipientActor() of
// browser-fullScreenAndPointerLock.js, this is used to ensure the fullscreen
// cleanup messages goes the same route as fullscreen request, especially for
// the cleanup that happens after actor is destroyed.
// TODO: Bug 1743703 - Consider moving the messaging component of
// browser-fullScreenAndPointerLock.js into the actor
nextMsgRecipient = null;
updateFullscreenWindowReference(aWindow) {
if (aWindow.document.documentElement.hasAttribute("inDOMFullscreen")) {
@ -19,35 +31,71 @@ class DOMFullscreenParent extends JSWindowActorParent {
}
}
didDestroy() {
let window = this._fullscreenWindow;
if (!window) {
cleanupDomFullscreen(aWindow) {
if (!aWindow.FullScreen) {
return;
}
if (this.waitingForChildFullscreen) {
// We were killed while waiting for our DOMFullscreenChild
// to transition to fullscreen so we abort the entire
// fullscreen transition to prevent getting stuck in a
// partial fullscreen state. We need to go through the
// document since window.Fullscreen could be undefined
// at this point.
//
// This could reject if we're not currently in fullscreen
// so just ignore rejection.
window.document.exitFullscreen().catch(() => {});
// If we don't need to wait for child reply, i.e. cleanupDomFullscreen
// doesn't message to child, and we've exit the fullscreen, there won't be
// DOMFullscreen:Painted message from child and it is possible that no more
// paint would be triggered, so just notify fullscreen-painted observer.
if (
!aWindow.FullScreen.cleanupDomFullscreen(this) &&
!aWindow.document.fullscreen
) {
Services.obs.notifyObservers(aWindow, "fullscreen-painted");
}
}
didDestroy() {
this._didDestroy = true;
let window = this._fullscreenWindow;
if (!window) {
if (this.waitingForChildExitFullscreen) {
this.waitingForChildExitFullscreen = false;
// We were destroyed while waiting for our DOMFullscreenChild to exit
// and have exited fullscreen, run cleanup steps anyway.
let topBrowsingContext = this.browsingContext.top;
let browser = topBrowsingContext.embedderElement;
if (browser) {
this.cleanupDomFullscreen(browser.ownerGlobal);
}
}
return;
}
if (this.waitingForChildEnterFullscreen) {
this.waitingForChildEnterFullscreen = false;
if (window.document.fullscreen) {
// We were destroyed while waiting for our DOMFullscreenChild
// to transition to fullscreen so we abort the entire
// fullscreen transition to prevent getting stuck in a
// partial fullscreen state. We need to go through the
// document since window.Fullscreen could be undefined
// at this point.
//
// This could reject if we're not currently in fullscreen
// so just ignore rejection.
window.document.exitFullscreen().catch(() => {});
return;
}
this.cleanupDomFullscreen(window);
}
// Need to resume Chrome UI if the window is still in fullscreen UI
// to avoid the window stays in fullscreen problem. (See Bug 1620341)
if (window.document.documentElement.hasAttribute("inDOMFullscreen")) {
if (window.FullScreen) {
window.FullScreen.cleanupDomFullscreen(this);
}
this.cleanupDomFullscreen(window);
if (window.windowUtils) {
window.windowUtils.remoteFrameFullscreenReverted();
}
} else if (this.waitingForChildExitFullscreen) {
this.waitingForChildExitFullscreen = false;
// We were destroyed while waiting for our DOMFullscreenChild to exit and
// have exited fullscreen, run cleanup steps anyway.
this.cleanupDomFullscreen(window);
}
this.updateFullscreenWindowReference(window);
}
@ -65,6 +113,8 @@ class DOMFullscreenParent extends JSWindowActorParent {
let window = browser.ownerGlobal;
switch (aMessage.name) {
case "DOMFullscreen:Request": {
this.waitingForChildExitFullscreen = false;
this.nextMsgRecipient = null;
this.requestOrigin = this;
this.addListeners(window);
window.windowUtils.remoteFrameFullscreenChanged(browser);
@ -77,24 +127,29 @@ class DOMFullscreenParent extends JSWindowActorParent {
aMessage.data.originNoSuffix
);
}
this.updateFullscreenWindowReference(window);
break;
}
case "DOMFullscreen:Entered": {
this.waitingForChildFullscreen = false;
this.nextMsgRecipient = null;
this.waitingForChildEnterFullscreen = false;
window.FullScreen.enterDomFullscreen(browser, this);
this.updateFullscreenWindowReference(window);
break;
}
case "DOMFullscreen:Exit": {
this.waitingForChildEnterFullscreen = false;
window.windowUtils.remoteFrameFullscreenReverted();
break;
}
case "DOMFullscreen:Exited": {
window.FullScreen.cleanupDomFullscreen(this);
this.waitingForChildExitFullscreen = false;
this.cleanupDomFullscreen(window);
this.updateFullscreenWindowReference(window);
break;
}
case "DOMFullscreen:Painted": {
this.waitingForChildExitFullscreen = false;
Services.obs.notifyObservers(window, "fullscreen-painted");
this.sendAsyncMessage("DOMFullscreen:Painted", {});
TelemetryStopwatch.finish("FULLSCREEN_CHANGE_MS");
@ -143,7 +198,7 @@ class DOMFullscreenParent extends JSWindowActorParent {
if (!this.hasBeenDestroyed() && !this.requestOrigin) {
this.requestOrigin = this;
}
window.FullScreen.cleanupDomFullscreen(this);
this.cleanupDomFullscreen(window);
this.updateFullscreenWindowReference(window);
this.removeListeners(window);
break;
@ -197,6 +252,10 @@ class DOMFullscreenParent extends JSWindowActorParent {
}
hasBeenDestroyed() {
if (this._didDestroy) {
return true;
}
// The 'didDestroy' callback is not always getting called.
// So we can't rely on it here. Instead, we will try to access
// the browsing context to judge wether the actor has

View File

@ -478,10 +478,10 @@ var FullScreen = {
remoteFrameBC: inProcessBC,
});
// Record that the actor is waiting for its child to enter
// fullscreen so that if it dies we can abort.
targetActor.waitingForChildFullscreen = true;
if (inProcessBC) {
// Record that the actor is waiting for its child to enter
// fullscreen so that if it dies we can abort.
targetActor.waitingForChildEnterFullscreen = true;
// We aren't messaging the request origin yet, skip this time.
return;
}
@ -557,13 +557,20 @@ var FullScreen = {
* the cleanup.
*/
cleanupDomFullscreen(aActor) {
let needToWaitForChildExit = false;
let [target, inProcessBC] = this._getNextMsgRecipientActor(aActor);
if (target) {
target.sendAsyncMessage("DOMFullscreen:CleanUp", {
remoteFrameBC: inProcessBC,
});
needToWaitForChildExit = true;
if (!target.waitingForChildExitFullscreen) {
// Record that the actor is waiting for its child to exit fullscreen so
// that if it dies we can continue cleanup.
target.waitingForChildExitFullscreen = true;
target.sendAsyncMessage("DOMFullscreen:CleanUp", {
remoteFrameBC: inProcessBC,
});
}
if (inProcessBC) {
return;
return needToWaitForChildExit;
}
}
@ -580,12 +587,17 @@ var FullScreen = {
);
document.documentElement.removeAttribute("inDOMFullscreen");
return needToWaitForChildExit;
},
_abortEnterFullscreen() {
// This function is called synchronously in fullscreen change, so
// we have to avoid calling exitFullscreen synchronously here.
setTimeout(() => document.exitFullscreen(), 0);
//
// This could reject if we're not currently in fullscreen
// so just ignore rejection.
setTimeout(() => document.exitFullscreen().catch(() => {}), 0);
if (TelemetryStopwatch.running("FULLSCREEN_CHANGE_MS")) {
// Cancel the stopwatch for any fullscreen change to avoid
// errors if it is started again.
@ -607,9 +619,27 @@ var FullScreen = {
* in process browsing context which is its child. Will be
* [null, null] if there is no OOP parent actor and request origin
* is unset. [null, null] is also returned if the intended actor or
* the calling actor has been destroyed.
* the calling actor has been destroyed or its associated
* WindowContext is in BFCache.
*/
_getNextMsgRecipientActor(aActor) {
// Walk up the cached nextMsgRecipient to find the next available actor if
// any.
if (aActor.nextMsgRecipient) {
let nextMsgRecipient = aActor.nextMsgRecipient;
while (nextMsgRecipient) {
let [actor] = nextMsgRecipient;
if (
!actor.hasBeenDestroyed() &&
actor.windowContext &&
!actor.windowContext.isInBFCache
) {
return nextMsgRecipient;
}
nextMsgRecipient = actor.nextMsgRecipient;
}
}
if (aActor.hasBeenDestroyed()) {
return [null, null];
}
@ -640,11 +670,16 @@ var FullScreen = {
if (parentBC && parentBC.currentWindowGlobal) {
target = parentBC.currentWindowGlobal.getActor("DOMFullscreen");
inProcessBC = childBC;
aActor.nextMsgRecipient = [target, inProcessBC];
} else {
target = aActor.requestOrigin;
}
if (!target || target.hasBeenDestroyed()) {
if (
!target ||
target.hasBeenDestroyed() ||
target.windowContext?.isInBFCache
) {
return [null, null];
}
return [target, inProcessBC];