mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-09 11:25:00 +00:00
Bug 1476555 - Show notification when autoplay blocked globally. r=cpearce,johannh
MozReview-Commit-ID: EI0GiaoBNqX --HG-- extra : rebase_source : 0c6fb98047913fc50423532cc4c433c4627c5b06
This commit is contained in:
parent
2456972415
commit
59e904f6cd
@ -1015,7 +1015,9 @@ var gIdentityHandler = {
|
||||
let nameLabelId = "identity-popup-permission-label-" + aPermission.id;
|
||||
nameLabel.setAttribute("id", nameLabelId);
|
||||
|
||||
let isPolicyPermission = aPermission.scope == SitePermissions.SCOPE_POLICY;
|
||||
let isPolicyPermission = [
|
||||
SitePermissions.SCOPE_POLICY, SitePermissions.SCOPE_GLOBAL
|
||||
].includes(aPermission.scope);
|
||||
|
||||
if (aPermission.id == "popup" && !isPolicyPermission) {
|
||||
let menulist = document.createXULElement("menulist");
|
||||
@ -1083,8 +1085,8 @@ var gIdentityHandler = {
|
||||
container.setAttribute("aria-labelledby", nameLabelId + " " + stateLabelId);
|
||||
|
||||
/* We return the permission item here without a remove button if the permission is a
|
||||
SCOPE_POLICY permission. Policy permissions cannot be removed/changed for the duration
|
||||
of the browser session. */
|
||||
SCOPE_POLICY or SCOPE_GLOBAL permission. Policy permissions cannot be
|
||||
removed/changed for the duration of the browser session. */
|
||||
if (isPolicyPermission) {
|
||||
return container;
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ function initRow(aPartId) {
|
||||
command.setAttribute("disabled", "true");
|
||||
}
|
||||
|
||||
if (scope == SitePermissions.SCOPE_POLICY) {
|
||||
if ([SitePermissions.SCOPE_POLICY, SitePermissions.SCOPE_GLOBAL].includes(scope)) {
|
||||
checkbox.setAttribute("disabled", "true");
|
||||
command.setAttribute("disabled", "true");
|
||||
}
|
||||
|
@ -10,6 +10,10 @@ support-files=
|
||||
support-files =
|
||||
temporary_permissions_subframe.html
|
||||
../webrtc/get_user_media.html
|
||||
[browser_autoplay_blocked.js]
|
||||
support-files =
|
||||
browser_autoplay_blocked.html
|
||||
../general/audio.ogg
|
||||
[browser_temporary_permissions_expiry.js]
|
||||
[browser_temporary_permissions_navigation.js]
|
||||
[browser_temporary_permissions_tabs.js]
|
||||
|
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE HTML>
|
||||
<!-- 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/. -->
|
||||
<html dir="ltr" xml:lang="en-US" lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
</head>
|
||||
<body>
|
||||
<audio autoplay="autoplay" >
|
||||
<source src="audio.ogg" />
|
||||
</audio>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Test that a blocked request to autoplay media is shown to the user
|
||||
*/
|
||||
|
||||
const AUTOPLAY_PAGE = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "https://example.com") + "browser_autoplay_blocked.html";
|
||||
|
||||
function openIdentityPopup() {
|
||||
let promise = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popupshown");
|
||||
gIdentityHandler._identityBox.click();
|
||||
return promise;
|
||||
}
|
||||
|
||||
function closeIdentityPopup() {
|
||||
let promise = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popuphidden");
|
||||
gIdentityHandler._identityPopup.hidePopup();
|
||||
return promise;
|
||||
}
|
||||
|
||||
function autoplayBlockedIcon() {
|
||||
return document.querySelector("#blocked-permissions-container " +
|
||||
".blocked-permission-icon.autoplay-media-icon");
|
||||
}
|
||||
|
||||
add_task(async function testMainViewVisible() {
|
||||
|
||||
Services.prefs.setIntPref("media.autoplay.default", Ci.nsIAutoplay.ALLOWED);
|
||||
|
||||
await BrowserTestUtils.withNewTab(AUTOPLAY_PAGE, async function() {
|
||||
let permissionsList = document.getElementById("identity-popup-permission-list");
|
||||
let emptyLabel = permissionsList.nextSibling.nextSibling;
|
||||
|
||||
ok(BrowserTestUtils.is_hidden(autoplayBlockedIcon()), "Blocked icon not shown");
|
||||
|
||||
await openIdentityPopup();
|
||||
ok(!BrowserTestUtils.is_hidden(emptyLabel), "List of permissions is empty");
|
||||
await closeIdentityPopup();
|
||||
});
|
||||
|
||||
Services.prefs.setIntPref("media.autoplay.default", Ci.nsIAutoplay.BLOCKED);
|
||||
|
||||
await BrowserTestUtils.withNewTab(AUTOPLAY_PAGE, async function() {
|
||||
let permissionsList = document.getElementById("identity-popup-permission-list");
|
||||
let emptyLabel = permissionsList.nextSibling.nextSibling;
|
||||
|
||||
ok(!BrowserTestUtils.is_hidden(autoplayBlockedIcon()), "Blocked icon is shown");
|
||||
|
||||
await openIdentityPopup();
|
||||
ok(BrowserTestUtils.is_hidden(emptyLabel), "List of permissions is not empty");
|
||||
let labelText = SitePermissions.getPermissionLabel("autoplay-media");
|
||||
let labels = permissionsList.querySelectorAll(".identity-popup-permission-label");
|
||||
is(labels.length, 1, "One permission visible in main view");
|
||||
is(labels[0].textContent, labelText, "Correct value");
|
||||
await closeIdentityPopup();
|
||||
});
|
||||
|
||||
Services.prefs.clearUserPref("media.autoplay.default");
|
||||
});
|
@ -279,6 +279,19 @@ var PermissionPromptPrototype = {
|
||||
this.browser);
|
||||
|
||||
if (state == SitePermissions.BLOCK) {
|
||||
// If the request is blocked by a global setting then we record
|
||||
// a flag that lasts for the duration of the current page load
|
||||
// to notify the user that the permission has been blocked.
|
||||
// Currently only applies to autoplay-media
|
||||
if (state == SitePermissions.getDefault(this.permissionKey) &&
|
||||
SitePermissions.showGloballyBlocked(this.permissionKey)) {
|
||||
SitePermissions.set(this.principal.URI,
|
||||
this.permissionKey,
|
||||
state,
|
||||
SitePermissions.SCOPE_GLOBAL,
|
||||
this.browser);
|
||||
}
|
||||
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
|
@ -129,6 +129,71 @@ const TemporaryBlockedPermissions = {
|
||||
},
|
||||
};
|
||||
|
||||
// This hold a flag per browser to indicate whether we should show the
|
||||
// user a notification as a permission has been requested that has been
|
||||
// blocked globally. We only want to notify the user in the case that
|
||||
// they actually requested the permission within the current page load
|
||||
// so will clear the flag on navigation.
|
||||
const GloballyBlockedPermissions = {
|
||||
|
||||
_stateByBrowser: new WeakMap(),
|
||||
|
||||
set(browser, id) {
|
||||
if (!this._stateByBrowser.has(browser)) {
|
||||
this._stateByBrowser.set(browser, {});
|
||||
}
|
||||
let entry = this._stateByBrowser.get(browser);
|
||||
let prePath = browser.currentURI.prePath;
|
||||
if (!entry[prePath]) {
|
||||
entry[prePath] = {};
|
||||
}
|
||||
|
||||
entry[prePath][id] = true;
|
||||
|
||||
// Listen to any top level navigations, once we see one clear the flag
|
||||
// and remove the listener.
|
||||
browser.addProgressListener({
|
||||
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener,
|
||||
Ci.nsISupportsWeakReference]),
|
||||
onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
|
||||
if (aWebProgress.isTopLevel) {
|
||||
GloballyBlockedPermissions.remove(browser, id);
|
||||
browser.removeProgressListener(this);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// Removes a permission with the specified id for the specified browser.
|
||||
remove(browser, id) {
|
||||
let entry = this._stateByBrowser.get(browser);
|
||||
let prePath = browser.currentURI.prePath;
|
||||
if (entry && entry[prePath]) {
|
||||
delete entry[prePath][id];
|
||||
}
|
||||
},
|
||||
|
||||
// Gets all permissions for the specified browser.
|
||||
// Note that only permissions that apply to the current URI
|
||||
// of the passed browser element will be returned.
|
||||
getAll(browser) {
|
||||
let permissions = [];
|
||||
let entry = this._stateByBrowser.get(browser);
|
||||
let prePath = browser.currentURI.prePath;
|
||||
if (entry && entry[prePath]) {
|
||||
let timeStamps = entry[prePath];
|
||||
for (let id of Object.keys(timeStamps)) {
|
||||
permissions.push({
|
||||
id,
|
||||
state: SitePermissions.BLOCK,
|
||||
scope: SitePermissions.SCOPE_GLOBAL
|
||||
});
|
||||
}
|
||||
}
|
||||
return permissions;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A module to manage permanent and temporary permissions
|
||||
* by URI and browser.
|
||||
@ -153,6 +218,7 @@ var SitePermissions = {
|
||||
SCOPE_SESSION: "{SitePermissions.SCOPE_SESSION}",
|
||||
SCOPE_PERSISTENT: "{SitePermissions.SCOPE_PERSISTENT}",
|
||||
SCOPE_POLICY: "{SitePermissions.SCOPE_POLICY}",
|
||||
SCOPE_GLOBAL: "{SitePermissions.SCOPE_GLOBAL}",
|
||||
|
||||
_defaultPrefBranch: Services.prefs.getBranch("permissions.default."),
|
||||
|
||||
@ -231,6 +297,10 @@ var SitePermissions = {
|
||||
permissions[permission.id] = permission;
|
||||
}
|
||||
|
||||
for (let permission of GloballyBlockedPermissions.getAll(browser)) {
|
||||
permissions[permission.id] = permission;
|
||||
}
|
||||
|
||||
for (let permission of this.getAllByURI(browser.currentURI)) {
|
||||
permissions[permission.id] = permission;
|
||||
}
|
||||
@ -330,6 +400,23 @@ var SitePermissions = {
|
||||
return this._defaultPrefBranch.getIntPref(permissionID, this.UNKNOWN);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return whether the browser should notify the user if a permission was
|
||||
* globally blocked due to a preference.
|
||||
*
|
||||
* @param {string} permissionID
|
||||
* The ID to get the state for.
|
||||
*
|
||||
* @return boolean Whether to show notification for globally blocked permissions.
|
||||
*/
|
||||
showGloballyBlocked(permissionID) {
|
||||
if (permissionID in gPermissionObject &&
|
||||
gPermissionObject[permissionID].showGloballyBlocked)
|
||||
return gPermissionObject[permissionID].showGloballyBlocked;
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the state and scope of a particular permission for a given URI.
|
||||
*
|
||||
@ -404,6 +491,13 @@ var SitePermissions = {
|
||||
* This needs to be provided if the scope is SCOPE_TEMPORARY!
|
||||
*/
|
||||
set(uri, permissionID, state, scope = this.SCOPE_PERSISTENT, browser = null) {
|
||||
|
||||
if (scope == this.SCOPE_GLOBAL && state == this.BLOCK) {
|
||||
GloballyBlockedPermissions.set(browser, permissionID);
|
||||
browser.dispatchEvent(new browser.ownerGlobal.CustomEvent("PermissionStateChange"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == this.UNKNOWN || state == this.getDefault(permissionID)) {
|
||||
// Because they are controlled by two prefs with many states that do not
|
||||
// correspond to the classical ALLOW/DENY/PROMPT model, we want to always
|
||||
@ -607,13 +701,14 @@ var gPermissionObject = {
|
||||
|
||||
"autoplay-media": {
|
||||
exactHostMatch: true,
|
||||
showGloballyBlocked: true,
|
||||
getDefault() {
|
||||
let state = Services.prefs.getIntPref("media.autoplay.default",
|
||||
Ci.nsIAutoplay.PROMPT);
|
||||
if (state == Ci.nsIAutoplay.ALLOW) {
|
||||
if (state == Ci.nsIAutoplay.ALLOWED) {
|
||||
return SitePermissions.ALLOW;
|
||||
} if (state == Ci.nsIAutoplay.BLOCK) {
|
||||
return SitePermissions.DENY;
|
||||
} if (state == Ci.nsIAutoplay.BLOCKED) {
|
||||
return SitePermissions.BLOCK;
|
||||
}
|
||||
return SitePermissions.UNKNOWN;
|
||||
},
|
||||
|
@ -2010,7 +2010,7 @@ HTMLMediaElement::Load()
|
||||
HasSourceChildren(this),
|
||||
EventStateManager::IsHandlingUserInput(),
|
||||
HasAttr(kNameSpaceID_None, nsGkAtoms::autoplay),
|
||||
AutoplayPolicy::IsAllowedToPlay(*this) == nsIAutoplay::ALLOWED,
|
||||
AutoplayPolicy::IsAllowedToPlay(*this),
|
||||
OwnerDoc(),
|
||||
DocumentOrigin(OwnerDoc()).get(),
|
||||
OwnerDoc() ? OwnerDoc()->HasBeenUserGestureActivated() : 0,
|
||||
@ -2529,7 +2529,7 @@ HTMLMediaElement::ResumeLoad(PreloadAction aAction)
|
||||
bool
|
||||
HTMLMediaElement::AllowedToPlay() const
|
||||
{
|
||||
return AutoplayPolicy::IsAllowedToPlay(*this) == nsIAutoplay::ALLOWED;
|
||||
return AutoplayPolicy::IsAllowedToPlay(*this);
|
||||
}
|
||||
|
||||
void
|
||||
@ -2538,7 +2538,7 @@ HTMLMediaElement::UpdatePreloadAction()
|
||||
PreloadAction nextAction = PRELOAD_UNDEFINED;
|
||||
// If autoplay is set, or we're playing, we should always preload data,
|
||||
// as we'll need it to play.
|
||||
if ((AutoplayPolicy::IsAllowedToPlay(*this) == nsIAutoplay::ALLOWED &&
|
||||
if ((AutoplayPolicy::IsAllowedToPlay(*this) &&
|
||||
HasAttr(kNameSpaceID_None, nsGkAtoms::autoplay)) ||
|
||||
!mPaused) {
|
||||
nextAction = HTMLMediaElement::PRELOAD_ENOUGH;
|
||||
@ -3071,7 +3071,7 @@ HTMLMediaElement::PauseIfShouldNotBePlaying()
|
||||
if (GetPaused()) {
|
||||
return;
|
||||
}
|
||||
if (AutoplayPolicy::IsAllowedToPlay(*this) != nsIAutoplay::ALLOWED) {
|
||||
if (!AutoplayPolicy::IsAllowedToPlay(*this)) {
|
||||
AUTOPLAY_LOG("pause because not allowed to play, element=%p", this);
|
||||
ErrorResult rv;
|
||||
Pause(rv);
|
||||
@ -4103,27 +4103,14 @@ HTMLMediaElement::Play(ErrorResult& aRv)
|
||||
UpdateHadAudibleAutoplayState();
|
||||
|
||||
const bool handlingUserInput = EventStateManager::IsHandlingUserInput();
|
||||
switch (AutoplayPolicy::IsAllowedToPlay(*this)) {
|
||||
case nsIAutoplay::ALLOWED: {
|
||||
mPendingPlayPromises.AppendElement(promise);
|
||||
PlayInternal(handlingUserInput);
|
||||
UpdateCustomPolicyAfterPlayed();
|
||||
break;
|
||||
}
|
||||
case nsIAutoplay::BLOCKED: {
|
||||
AUTOPLAY_LOG("%p play blocked.", this);
|
||||
promise->MaybeReject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR);
|
||||
if (StaticPrefs::MediaBlockEventEnabled()) {
|
||||
DispatchAsyncEvent(NS_LITERAL_STRING("blocked"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case nsIAutoplay::PROMPT: {
|
||||
// Prompt the user for permission to play.
|
||||
mPendingPlayPromises.AppendElement(promise);
|
||||
EnsureAutoplayRequested(handlingUserInput);
|
||||
break;
|
||||
}
|
||||
if (AutoplayPolicy::IsAllowedToPlay(*this)) {
|
||||
mPendingPlayPromises.AppendElement(promise);
|
||||
PlayInternal(handlingUserInput);
|
||||
UpdateCustomPolicyAfterPlayed();
|
||||
} else {
|
||||
// Prompt the user for permission to play.
|
||||
mPendingPlayPromises.AppendElement(promise);
|
||||
EnsureAutoplayRequested(handlingUserInput);
|
||||
}
|
||||
return promise.forget();
|
||||
}
|
||||
@ -6151,8 +6138,7 @@ HTMLMediaElement::ChangeReadyState(nsMediaReadyState aState)
|
||||
DispatchAsyncEvent(NS_LITERAL_STRING("canplay"));
|
||||
if (!mPaused) {
|
||||
if (mDecoder && !mPausedForInactiveDocumentOrChannel) {
|
||||
MOZ_ASSERT(AutoplayPolicy::IsAllowedToPlay(*this) ==
|
||||
nsIAutoplay::ALLOWED);
|
||||
MOZ_ASSERT(AutoplayPolicy::IsAllowedToPlay(*this));
|
||||
mDecoder->Play();
|
||||
}
|
||||
NotifyAboutPlaying();
|
||||
@ -6264,14 +6250,9 @@ HTMLMediaElement::CheckAutoplayDataReady()
|
||||
}
|
||||
|
||||
UpdateHadAudibleAutoplayState();
|
||||
switch (AutoplayPolicy::IsAllowedToPlay(*this)) {
|
||||
case nsIAutoplay::BLOCKED:
|
||||
return;
|
||||
case nsIAutoplay::PROMPT:
|
||||
EnsureAutoplayRequested(false);
|
||||
return;
|
||||
case nsIAutoplay::ALLOWED:
|
||||
break;
|
||||
if (!AutoplayPolicy::IsAllowedToPlay(*this)) {
|
||||
EnsureAutoplayRequested(false);
|
||||
return;
|
||||
}
|
||||
|
||||
mPaused = false;
|
||||
|
@ -156,7 +156,7 @@ AutoplayPolicy::WouldBeAllowedToPlayIfAutoplayDisabled(const HTMLMediaElement& a
|
||||
return IsMediaElementAllowedToPlay(aElement);
|
||||
}
|
||||
|
||||
/* static */ uint32_t
|
||||
/* static */ bool
|
||||
AutoplayPolicy::IsAllowedToPlay(const HTMLMediaElement& aElement)
|
||||
{
|
||||
const uint32_t autoplayDefault = DefaultAutoplayBehaviour();
|
||||
@ -166,15 +166,19 @@ AutoplayPolicy::IsAllowedToPlay(const HTMLMediaElement& aElement)
|
||||
// If element is blessed, it would always be allowed to play().
|
||||
return (autoplayDefault == nsIAutoplay::ALLOWED ||
|
||||
aElement.IsBlessed() ||
|
||||
EventStateManager::IsHandlingUserInput())
|
||||
? nsIAutoplay::ALLOWED : nsIAutoplay::BLOCKED;
|
||||
EventStateManager::IsHandlingUserInput());
|
||||
}
|
||||
|
||||
const uint32_t result = IsMediaElementAllowedToPlay(aElement) ?
|
||||
nsIAutoplay::ALLOWED : autoplayDefault;
|
||||
if (IsMediaElementAllowedToPlay(aElement)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool result = IsMediaElementAllowedToPlay(aElement) ||
|
||||
autoplayDefault == nsIAutoplay::ALLOWED;
|
||||
|
||||
AUTOPLAY_LOG("IsAllowedToPlay, mediaElement=%p, isAllowToPlay=%s",
|
||||
&aElement, AllowAutoplayToStr(result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ class AutoplayPolicy
|
||||
{
|
||||
public:
|
||||
// Returns whether a given media element is allowed to play.
|
||||
static uint32_t IsAllowedToPlay(const HTMLMediaElement& aElement);
|
||||
static bool IsAllowedToPlay(const HTMLMediaElement& aElement);
|
||||
|
||||
// Returns true if a given media element would be allowed to play
|
||||
// if block autoplay was enabled. If this returns false, it means we would
|
||||
|
Loading…
Reference in New Issue
Block a user