From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- dom/media/autoplay/AutoplayPolicy.cpp | 497 +++++++++++++++++++++ dom/media/autoplay/AutoplayPolicy.h | 80 ++++ dom/media/autoplay/GVAutoplayPermissionRequest.cpp | 236 ++++++++++ dom/media/autoplay/GVAutoplayPermissionRequest.h | 86 ++++ dom/media/autoplay/GVAutoplayRequestStatusIPC.h | 23 + dom/media/autoplay/GVAutoplayRequestUtils.h | 25 ++ dom/media/autoplay/moz.build | 32 ++ dom/media/autoplay/nsIAutoplay.idl | 17 + dom/media/autoplay/test/browser/audio.ogg | Bin 0 -> 14290 bytes dom/media/autoplay/test/browser/browser.ini | 27 ++ ...wser_autoplay_policy_detection_click_to_play.js | 120 +++++ ...play_policy_detection_global_and_site_sticky.js | 168 +++++++ ...wser_autoplay_policy_detection_global_sticky.js | 105 +++++ .../browser/browser_autoplay_policy_play_twice.js | 54 +++ .../browser_autoplay_policy_request_permission.js | 269 +++++++++++ .../browser/browser_autoplay_policy_touchScroll.js | 103 +++++ .../browser_autoplay_policy_user_gestures.js | 277 ++++++++++++ .../browser_autoplay_policy_webRTC_permission.js | 67 +++ .../browser/browser_autoplay_policy_web_audio.js | 217 +++++++++ .../browser_autoplay_policy_web_audio_with_gum.js | 174 ++++++++ .../test/browser/browser_autoplay_videoDocument.js | 80 ++++ dom/media/autoplay/test/browser/file_empty.html | 8 + .../test/browser/file_mediaplayback_frame.html | 21 + .../test/browser/file_nonAutoplayAudio.html | 7 + dom/media/autoplay/test/browser/file_video.html | 9 + dom/media/autoplay/test/browser/head.js | 149 ++++++ .../autoplay/test/mochitest/AutoplayTestUtils.js | 46 ++ .../file_autoplay_gv_play_request_frame.html | 24 + .../file_autoplay_gv_play_request_window.html | 65 +++ .../file_autoplay_policy_activation_frame.html | 32 ++ .../file_autoplay_policy_activation_window.html | 80 ++++ .../file_autoplay_policy_eventdown_activation.html | 85 ++++ .../file_autoplay_policy_key_blacklist.html | 148 ++++++ ...autoplay_policy_play_before_loadedmetadata.html | 63 +++ .../file_autoplay_policy_unmute_pauses.html | 65 +++ dom/media/autoplay/test/mochitest/mochitest.ini | 54 +++ .../autoplay/test/mochitest/test_autoplay.html | 36 ++ .../mochitest/test_autoplay_contentEditable.html | 67 +++ .../mochitest/test_autoplay_gv_play_request.html | 221 +++++++++ .../test/mochitest/test_autoplay_policy.html | 174 ++++++++ .../mochitest/test_autoplay_policy_activation.html | 180 ++++++++ .../test_autoplay_policy_eventdown_activation.html | 55 +++ .../test_autoplay_policy_key_blacklist.html | 47 ++ .../mochitest/test_autoplay_policy_permission.html | 80 ++++ ...autoplay_policy_play_before_loadedmetadata.html | 73 +++ .../test_autoplay_policy_unmute_pauses.html | 64 +++ ...autoplay_policy_web_audio_AudioParamStream.html | 171 +++++++ ...y_policy_web_audio_createMediaStreamSource.html | 119 +++++ ...licy_web_audio_mediaElementAudioSourceNode.html | 105 +++++ ..._notResumePageInvokedSuspendedAudioContext.html | 96 ++++ .../test/mochitest/test_streams_autoplay.html | 47 ++ 51 files changed, 5048 insertions(+) create mode 100644 dom/media/autoplay/AutoplayPolicy.cpp create mode 100644 dom/media/autoplay/AutoplayPolicy.h create mode 100644 dom/media/autoplay/GVAutoplayPermissionRequest.cpp create mode 100644 dom/media/autoplay/GVAutoplayPermissionRequest.h create mode 100644 dom/media/autoplay/GVAutoplayRequestStatusIPC.h create mode 100644 dom/media/autoplay/GVAutoplayRequestUtils.h create mode 100644 dom/media/autoplay/moz.build create mode 100644 dom/media/autoplay/nsIAutoplay.idl create mode 100644 dom/media/autoplay/test/browser/audio.ogg create mode 100644 dom/media/autoplay/test/browser/browser.ini create mode 100644 dom/media/autoplay/test/browser/browser_autoplay_policy_detection_click_to_play.js create mode 100644 dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_and_site_sticky.js create mode 100644 dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_sticky.js create mode 100644 dom/media/autoplay/test/browser/browser_autoplay_policy_play_twice.js create mode 100644 dom/media/autoplay/test/browser/browser_autoplay_policy_request_permission.js create mode 100644 dom/media/autoplay/test/browser/browser_autoplay_policy_touchScroll.js create mode 100644 dom/media/autoplay/test/browser/browser_autoplay_policy_user_gestures.js create mode 100644 dom/media/autoplay/test/browser/browser_autoplay_policy_webRTC_permission.js create mode 100644 dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio.js create mode 100644 dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio_with_gum.js create mode 100644 dom/media/autoplay/test/browser/browser_autoplay_videoDocument.js create mode 100644 dom/media/autoplay/test/browser/file_empty.html create mode 100644 dom/media/autoplay/test/browser/file_mediaplayback_frame.html create mode 100644 dom/media/autoplay/test/browser/file_nonAutoplayAudio.html create mode 100644 dom/media/autoplay/test/browser/file_video.html create mode 100644 dom/media/autoplay/test/browser/head.js create mode 100644 dom/media/autoplay/test/mochitest/AutoplayTestUtils.js create mode 100644 dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_frame.html create mode 100644 dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_window.html create mode 100644 dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_frame.html create mode 100644 dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_window.html create mode 100644 dom/media/autoplay/test/mochitest/file_autoplay_policy_eventdown_activation.html create mode 100644 dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html create mode 100644 dom/media/autoplay/test/mochitest/file_autoplay_policy_play_before_loadedmetadata.html create mode 100644 dom/media/autoplay/test/mochitest/file_autoplay_policy_unmute_pauses.html create mode 100644 dom/media/autoplay/test/mochitest/mochitest.ini create mode 100644 dom/media/autoplay/test/mochitest/test_autoplay.html create mode 100644 dom/media/autoplay/test/mochitest/test_autoplay_contentEditable.html create mode 100644 dom/media/autoplay/test/mochitest/test_autoplay_gv_play_request.html create mode 100644 dom/media/autoplay/test/mochitest/test_autoplay_policy.html create mode 100644 dom/media/autoplay/test/mochitest/test_autoplay_policy_activation.html create mode 100644 dom/media/autoplay/test/mochitest/test_autoplay_policy_eventdown_activation.html create mode 100644 dom/media/autoplay/test/mochitest/test_autoplay_policy_key_blacklist.html create mode 100644 dom/media/autoplay/test/mochitest/test_autoplay_policy_permission.html create mode 100644 dom/media/autoplay/test/mochitest/test_autoplay_policy_play_before_loadedmetadata.html create mode 100644 dom/media/autoplay/test/mochitest/test_autoplay_policy_unmute_pauses.html create mode 100644 dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_AudioParamStream.html create mode 100644 dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_createMediaStreamSource.html create mode 100644 dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html create mode 100644 dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html create mode 100644 dom/media/autoplay/test/mochitest/test_streams_autoplay.html (limited to 'dom/media/autoplay') diff --git a/dom/media/autoplay/AutoplayPolicy.cpp b/dom/media/autoplay/AutoplayPolicy.cpp new file mode 100644 index 0000000000..8da0e60ead --- /dev/null +++ b/dom/media/autoplay/AutoplayPolicy.cpp @@ -0,0 +1,497 @@ +/* -*- 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 "AutoplayPolicy.h" + +#include "mozilla/dom/AudioContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/FeaturePolicyUtils.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/HTMLMediaElementBinding.h" +#include "mozilla/dom/NavigatorBinding.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/Logging.h" +#include "mozilla/MediaManager.h" +#include "mozilla/Components.h" +#include "mozilla/StaticPrefs_media.h" +#include "nsContentUtils.h" +#include "nsGlobalWindowInner.h" +#include "nsIAutoplay.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeItem.h" +#include "nsIPermissionManager.h" +#include "nsIPrincipal.h" +#include "nsPIDOMWindow.h" + +mozilla::LazyLogModule gAutoplayPermissionLog("Autoplay"); + +#define AUTOPLAY_LOG(msg, ...) \ + MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +using namespace mozilla::dom; + +namespace mozilla::media { + +static const uint32_t sPOLICY_STICKY_ACTIVATION = 0; +// static const uint32_t sPOLICY_TRANSIENT_ACTIVATION = 1; +static const uint32_t sPOLICY_USER_INPUT_DEPTH = 2; + +static bool IsActivelyCapturingOrHasAPermission(nsPIDOMWindowInner* aWindow) { + // Pages which have been granted permission to capture WebRTC camera or + // microphone or screen are assumed to be trusted, and are allowed to + // autoplay. + if (MediaManager::GetIfExists()) { + return MediaManager::GetIfExists()->IsActivelyCapturingOrHasAPermission( + aWindow->WindowID()); + } + + auto principal = nsGlobalWindowInner::Cast(aWindow)->GetPrincipal(); + return (nsContentUtils::IsExactSitePermAllow(principal, "camera"_ns) || + nsContentUtils::IsExactSitePermAllow(principal, "microphone"_ns) || + nsContentUtils::IsExactSitePermAllow(principal, "screen"_ns)); +} + +static uint32_t SiteAutoplayPerm(nsPIDOMWindowInner* aWindow) { + if (!aWindow || !aWindow->GetBrowsingContext()) { + return nsIPermissionManager::UNKNOWN_ACTION; + } + + WindowContext* topContext = + aWindow->GetBrowsingContext()->GetTopWindowContext(); + if (!topContext) { + return nsIPermissionManager::UNKNOWN_ACTION; + } + return topContext->GetAutoplayPermission(); +} + +static bool IsWindowAllowedToPlayByUserGesture(nsPIDOMWindowInner* aWindow) { + if (!aWindow) { + return false; + } + + WindowContext* topContext = + aWindow->GetBrowsingContext()->GetTopWindowContext(); + if (topContext && topContext->HasBeenUserGestureActivated()) { + AUTOPLAY_LOG( + "Allow autoplay as top-level context has been activated by user " + "gesture."); + return true; + } + return false; +} + +static bool IsWindowAllowedToPlayByTraits(nsPIDOMWindowInner* aWindow) { + if (!aWindow) { + return false; + } + + if (IsActivelyCapturingOrHasAPermission(aWindow)) { + AUTOPLAY_LOG( + "Allow autoplay as document has camera or microphone or screen" + " permission."); + return true; + } + + Document* currentDoc = aWindow->GetExtantDoc(); + if (!currentDoc) { + return false; + } + + bool isTopLevelContent = !aWindow->GetBrowsingContext()->GetParent(); + if (currentDoc->MediaDocumentKind() == Document::MediaDocumentKind::Video && + isTopLevelContent) { + AUTOPLAY_LOG("Allow top-level video document to autoplay."); + return true; + } + + if (StaticPrefs::media_autoplay_allow_extension_background_pages() && + currentDoc->IsExtensionPage()) { + AUTOPLAY_LOG("Allow autoplay as in extension document."); + return true; + } + + return false; +} + +static bool IsWindowAllowedToPlayOverall(nsPIDOMWindowInner* aWindow) { + return IsWindowAllowedToPlayByUserGesture(aWindow) || + IsWindowAllowedToPlayByTraits(aWindow); +} + +static uint32_t DefaultAutoplayBehaviour() { + int32_t prefValue = StaticPrefs::media_autoplay_default(); + if (prefValue == nsIAutoplay::ALLOWED) { + return nsIAutoplay::ALLOWED; + } + if (prefValue == nsIAutoplay::BLOCKED_ALL) { + return nsIAutoplay::BLOCKED_ALL; + } + return nsIAutoplay::BLOCKED; +} + +static bool IsMediaElementInaudible(const HTMLMediaElement& aElement) { + if (aElement.Volume() == 0.0 || aElement.Muted()) { + AUTOPLAY_LOG("Media %p is muted.", &aElement); + return true; + } + + if (!aElement.HasAudio() && + aElement.ReadyState() >= HTMLMediaElement_Binding::HAVE_METADATA) { + AUTOPLAY_LOG("Media %p has no audio track", &aElement); + return true; + } + + return false; +} + +static bool IsAudioContextAllowedToPlay(const AudioContext& aContext) { + // Offline context won't directly output sound to audio devices. + return aContext.IsOffline() || + IsWindowAllowedToPlayOverall(aContext.GetParentObject()); +} + +static bool IsEnableBlockingWebAudioByUserGesturePolicy() { + return StaticPrefs::media_autoplay_block_webaudio() && + StaticPrefs::media_autoplay_blocking_policy() == + sPOLICY_STICKY_ACTIVATION; +} + +static bool IsAllowedToPlayByBlockingModel(const HTMLMediaElement& aElement) { + const uint32_t policy = StaticPrefs::media_autoplay_blocking_policy(); + if (policy == sPOLICY_STICKY_ACTIVATION) { + const bool isAllowed = + IsWindowAllowedToPlayOverall(aElement.OwnerDoc()->GetInnerWindow()); + AUTOPLAY_LOG("Use 'sticky-activation', isAllowed=%d", isAllowed); + return isAllowed; + } + // If element is blessed, it would always be allowed to play(). + const bool isElementBlessed = aElement.IsBlessed(); + if (policy == sPOLICY_USER_INPUT_DEPTH) { + const bool isUserInput = UserActivation::IsHandlingUserInput(); + AUTOPLAY_LOG("Use 'User-Input-Depth', isBlessed=%d, isUserInput=%d", + isElementBlessed, isUserInput); + return isElementBlessed || isUserInput; + } + const bool hasTransientActivation = + aElement.OwnerDoc()->HasValidTransientUserGestureActivation(); + AUTOPLAY_LOG( + "Use 'transient-activation', isBlessed=%d, " + "hasValidTransientActivation=%d", + isElementBlessed, hasTransientActivation); + return isElementBlessed || hasTransientActivation; +} + +// On GeckoView, we don't store any site's permission in permission manager, we +// would check the GV request status to know if the site can be allowed to play. +// But on other platforms, we would store the site's permission in permission +// manager. +#if defined(MOZ_WIDGET_ANDROID) +using RType = GVAutoplayRequestType; + +static bool IsGVAutoplayRequestAllowed(nsPIDOMWindowInner* aWindow, + RType aType) { + if (!aWindow) { + return false; + } + + RefPtr context = aWindow->GetBrowsingContext()->Top(); + GVAutoplayRequestStatus status = + aType == RType::eAUDIBLE ? context->GetGVAudibleAutoplayRequestStatus() + : context->GetGVInaudibleAutoplayRequestStatus(); + return status == GVAutoplayRequestStatus::eALLOWED; +} + +static bool IsGVAutoplayRequestAllowed(const HTMLMediaElement& aElement, + RType aType) { + // On GV, blocking model is the first thing we would check inside Gecko, and + // if the media is not allowed by that, then we would check the response from + // the embedding app to decide the final result. + if (IsAllowedToPlayByBlockingModel(aElement)) { + return true; + } + + RefPtr window = aElement.OwnerDoc()->GetInnerWindow(); + if (!window) { + return false; + } + return IsGVAutoplayRequestAllowed(window, aType); +} +#endif + +static bool IsAllowedToPlayInternal(const HTMLMediaElement& aElement) { +#if defined(MOZ_WIDGET_ANDROID) + if (StaticPrefs::media_geckoview_autoplay_request()) { + return IsGVAutoplayRequestAllowed( + aElement, IsMediaElementInaudible(aElement) ? RType::eINAUDIBLE + : RType::eAUDIBLE); + } +#endif + bool isInaudible = IsMediaElementInaudible(aElement); + bool isUsingAutoplayModel = IsAllowedToPlayByBlockingModel(aElement); + + uint32_t defaultBehaviour = DefaultAutoplayBehaviour(); + uint32_t sitePermission = + SiteAutoplayPerm(aElement.OwnerDoc()->GetInnerWindow()); + + AUTOPLAY_LOG( + "IsAllowedToPlayInternal, isInaudible=%d," + "isUsingAutoplayModel=%d, sitePermission=%d, defaultBehaviour=%d", + isInaudible, isUsingAutoplayModel, sitePermission, defaultBehaviour); + + // For site permissions we store permissionManager values except + // for BLOCKED_ALL, for the default pref values we store + // nsIAutoplay values. + if (sitePermission == nsIPermissionManager::ALLOW_ACTION) { + return true; + } + + if (sitePermission == nsIPermissionManager::DENY_ACTION) { + return isInaudible || isUsingAutoplayModel; + } + + if (sitePermission == nsIAutoplay::BLOCKED_ALL) { + return isUsingAutoplayModel; + } + + if (defaultBehaviour == nsIAutoplay::ALLOWED) { + return true; + } + + if (defaultBehaviour == nsIAutoplay::BLOCKED) { + return isInaudible || isUsingAutoplayModel; + } + + MOZ_ASSERT(defaultBehaviour == nsIAutoplay::BLOCKED_ALL); + return isUsingAutoplayModel; +} + +/* static */ +bool AutoplayPolicy::IsAllowedToPlay(const HTMLMediaElement& aElement) { + const bool result = IsAllowedToPlayInternal(aElement); + AUTOPLAY_LOG("IsAllowedToPlay, mediaElement=%p, isAllowToPlay=%s", &aElement, + result ? "allowed" : "blocked"); + return result; +} + +/* static */ +bool AutoplayPolicy::IsAllowedToPlay(const AudioContext& aContext) { + /** + * The autoplay checking has 5 different phases, + * 1. check whether audio context itself meets the autoplay condition + * 2. check if we enable blocking web audio or not + * (only support blocking when using user-gesture-activation model) + * 3. check whether the site is in the autoplay whitelist + * 4. check global autoplay setting and check wether the site is in the + * autoplay blacklist. + * 5. check whether media is allowed under current blocking model + * (only support user-gesture-activation model) + */ + if (aContext.IsOffline()) { + return true; + } + + if (!IsEnableBlockingWebAudioByUserGesturePolicy()) { + return true; + } + + nsPIDOMWindowInner* window = aContext.GetParentObject(); + uint32_t sitePermission = SiteAutoplayPerm(window); + + if (sitePermission == nsIPermissionManager::ALLOW_ACTION) { + AUTOPLAY_LOG( + "Allow autoplay as document has permanent autoplay permission."); + return true; + } + + if (DefaultAutoplayBehaviour() == nsIAutoplay::ALLOWED && + sitePermission != nsIPermissionManager::DENY_ACTION && + sitePermission != nsIAutoplay::BLOCKED_ALL) { + AUTOPLAY_LOG( + "Allow autoplay as global autoplay setting is allowing autoplay by " + "default."); + return true; + } + + return IsWindowAllowedToPlayOverall(window); +} + +enum class DocumentAutoplayPolicy : uint8_t { + Allowed, + Allowed_muted, + Disallowed +}; + +/* static */ +DocumentAutoplayPolicy IsDocAllowedToPlay(const Document& aDocument) { + RefPtr window = aDocument.GetInnerWindow(); + +#if defined(MOZ_WIDGET_ANDROID) + if (StaticPrefs::media_geckoview_autoplay_request()) { + const bool isWindowAllowedToPlay = IsWindowAllowedToPlayOverall(window); + if (IsGVAutoplayRequestAllowed(window, RType::eAUDIBLE)) { + return DocumentAutoplayPolicy::Allowed; + } + + if (IsGVAutoplayRequestAllowed(window, RType::eINAUDIBLE)) { + return isWindowAllowedToPlay ? DocumentAutoplayPolicy::Allowed + : DocumentAutoplayPolicy::Allowed_muted; + } + + return isWindowAllowedToPlay ? DocumentAutoplayPolicy::Allowed + : DocumentAutoplayPolicy::Disallowed; + } +#endif + const uint32_t sitePermission = SiteAutoplayPerm(window); + const uint32_t globalPermission = DefaultAutoplayBehaviour(); + const uint32_t policy = StaticPrefs::media_autoplay_blocking_policy(); + const bool isWindowAllowedToPlayByGesture = + policy != sPOLICY_USER_INPUT_DEPTH && + IsWindowAllowedToPlayByUserGesture(window); + const bool isWindowAllowedToPlayByTraits = + IsWindowAllowedToPlayByTraits(window); + + AUTOPLAY_LOG( + "IsDocAllowedToPlay(), policy=%d, sitePermission=%d, " + "globalPermission=%d, isWindowAllowedToPlayByGesture=%d, " + "isWindowAllowedToPlayByTraits=%d", + policy, sitePermission, globalPermission, isWindowAllowedToPlayByGesture, + isWindowAllowedToPlayByTraits); + + if ((globalPermission == nsIAutoplay::ALLOWED && + (sitePermission != nsIPermissionManager::DENY_ACTION && + sitePermission != nsIAutoplay::BLOCKED_ALL)) || + sitePermission == nsIPermissionManager::ALLOW_ACTION || + isWindowAllowedToPlayByGesture || isWindowAllowedToPlayByTraits) { + return DocumentAutoplayPolicy::Allowed; + } + + if ((globalPermission == nsIAutoplay::BLOCKED && + sitePermission != nsIAutoplay::BLOCKED_ALL) || + sitePermission == nsIPermissionManager::DENY_ACTION) { + return DocumentAutoplayPolicy::Allowed_muted; + } + + return DocumentAutoplayPolicy::Disallowed; +} + +/* static */ +uint32_t AutoplayPolicy::GetSiteAutoplayPermission(nsIPrincipal* aPrincipal) { + if (!aPrincipal) { + return nsIPermissionManager::DENY_ACTION; + } + + nsCOMPtr permMgr = + components::PermissionManager::Service(); + if (!permMgr) { + return nsIPermissionManager::DENY_ACTION; + } + + uint32_t perm = nsIPermissionManager::DENY_ACTION; + permMgr->TestExactPermissionFromPrincipal(aPrincipal, "autoplay-media"_ns, + &perm); + return perm; +} + +/* static */ +bool AutoplayPolicyTelemetryUtils::WouldBeAllowedToPlayIfAutoplayDisabled( + const AudioContext& aContext) { + return IsAudioContextAllowedToPlay(aContext); +} + +/* static */ +dom::AutoplayPolicy AutoplayPolicy::GetAutoplayPolicy( + const dom::HTMLMediaElement& aElement) { + // Note, the site permission can contain following values : + // - UNKNOWN_ACTION : no permission set for this site + // - ALLOW_ACTION : allowed to autoplay + // - DENY_ACTION : allowed inaudible autoplay, disallowed inaudible autoplay + // - nsIAutoplay::BLOCKED_ALL : autoplay disallowed + // and the global permissions would be nsIAutoplay::{BLOCKED, ALLOWED, + // BLOCKED_ALL} + const uint32_t sitePermission = + SiteAutoplayPerm(aElement.OwnerDoc()->GetInnerWindow()); + const uint32_t globalPermission = DefaultAutoplayBehaviour(); + const bool isAllowedToPlayByBlockingModel = + IsAllowedToPlayByBlockingModel(aElement); + + AUTOPLAY_LOG( + "IsAllowedToPlay(element), sitePermission=%d, globalPermission=%d, " + "isAllowedToPlayByBlockingModel=%d", + sitePermission, globalPermission, isAllowedToPlayByBlockingModel); + +#if defined(MOZ_WIDGET_ANDROID) + if (StaticPrefs::media_geckoview_autoplay_request()) { + if (IsGVAutoplayRequestAllowed(aElement, RType::eAUDIBLE)) { + return dom::AutoplayPolicy::Allowed; + } else if (IsGVAutoplayRequestAllowed(aElement, RType::eINAUDIBLE)) { + return isAllowedToPlayByBlockingModel + ? dom::AutoplayPolicy::Allowed + : dom::AutoplayPolicy::Allowed_muted; + } else { + return isAllowedToPlayByBlockingModel ? dom::AutoplayPolicy::Allowed + : dom::AutoplayPolicy::Disallowed; + } + } +#endif + + // These are situations when an element is allowed to autoplay + // 1. The site permission is explicitly allowed + // 2. The global permission is allowed, and the site isn't explicitly + // disallowed + // 3. The blocking model is explicitly allowed this element + if (sitePermission == nsIPermissionManager::ALLOW_ACTION || + (globalPermission == nsIAutoplay::ALLOWED && + (sitePermission != nsIPermissionManager::DENY_ACTION && + sitePermission != nsIAutoplay::BLOCKED_ALL)) || + isAllowedToPlayByBlockingModel) { + return dom::AutoplayPolicy::Allowed; + } + + // These are situations when a element is allowed to autoplay only when it's + // inaudible. + // 1. The site permission is block-audible-autoplay + // 2. The global permission is block-audible-autoplay, and the site permission + // isn't block-all-autoplay + if (sitePermission == nsIPermissionManager::DENY_ACTION || + (globalPermission == nsIAutoplay::BLOCKED && + sitePermission != nsIAutoplay::BLOCKED_ALL)) { + return dom::AutoplayPolicy::Allowed_muted; + } + + return dom::AutoplayPolicy::Disallowed; +} + +/* static */ +dom::AutoplayPolicy AutoplayPolicy::GetAutoplayPolicy( + const dom::AudioContext& aContext) { + if (AutoplayPolicy::IsAllowedToPlay(aContext)) { + return dom::AutoplayPolicy::Allowed; + } + return dom::AutoplayPolicy::Disallowed; +} + +/* static */ +dom::AutoplayPolicy AutoplayPolicy::GetAutoplayPolicy( + const dom::AutoplayPolicyMediaType& aType, const dom::Document& aDoc) { + DocumentAutoplayPolicy policy = IsDocAllowedToPlay(aDoc); + // https://w3c.github.io/autoplay/#query-by-a-media-type + if (aType == dom::AutoplayPolicyMediaType::Audiocontext) { + return policy == DocumentAutoplayPolicy::Allowed + ? dom::AutoplayPolicy::Allowed + : dom::AutoplayPolicy::Disallowed; + } + MOZ_ASSERT(aType == dom::AutoplayPolicyMediaType::Mediaelement); + if (policy == DocumentAutoplayPolicy::Allowed) { + return dom::AutoplayPolicy::Allowed; + } + if (policy == DocumentAutoplayPolicy::Allowed_muted) { + return dom::AutoplayPolicy::Allowed_muted; + } + return dom::AutoplayPolicy::Disallowed; +} + +} // namespace mozilla::media diff --git a/dom/media/autoplay/AutoplayPolicy.h b/dom/media/autoplay/AutoplayPolicy.h new file mode 100644 index 0000000000..c7b009f2d8 --- /dev/null +++ b/dom/media/autoplay/AutoplayPolicy.h @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#if !defined(AutoplayPolicy_h_) +# define AutoplayPolicy_h_ + +# include "mozilla/NotNull.h" + +class nsIPrincipal; + +namespace mozilla::dom { + +class HTMLMediaElement; +class AudioContext; +class Document; +enum class AutoplayPolicy : uint8_t; +enum class AutoplayPolicyMediaType : uint8_t; + +} // namespace mozilla::dom + +namespace mozilla::media { +/** + * AutoplayPolicy is used to manage autoplay logic for all kinds of media, + * including MediaElement, Web Audio and Web Speech. + * + * Autoplay could be disable by setting the pref "media.autoplay.default" + * to anything but nsIAutoplay::Allowed. Once user disables autoplay, media + * could only be played if one of following conditions is true. + * 1) Owner document is activated by user gestures + * We restrict user gestures to "mouse click", "keyboard press" and "touch". + * 2) Muted media content or video without audio content. + * 3) Document's origin has the "autoplay-media" permission. + */ +class AutoplayPolicy { + public: + // Returns whether a given media element is allowed to play. + static bool IsAllowedToPlay(const dom::HTMLMediaElement& aElement); + + // Returns whether a given AudioContext is allowed to play. + static bool IsAllowedToPlay(const dom::AudioContext& aContext); + + // Return the value of the autoplay permission for given principal. The return + // value can be 0=unknown, 1=allow, 2=block audio, 5=block audio and video. + static uint32_t GetSiteAutoplayPermission(nsIPrincipal* aPrincipal); + + // Following methods are used for the internal implementation for the Autoplay + // Policy Detection API, the public JS interfaces are in exposed on Navigator. + // https://w3c.github.io/autoplay/#autoplay-detection-methods + static dom::AutoplayPolicy GetAutoplayPolicy( + const dom::HTMLMediaElement& aElement); + + static dom::AutoplayPolicy GetAutoplayPolicy( + const dom::AudioContext& aContext); + + static dom::AutoplayPolicy GetAutoplayPolicy( + const dom::AutoplayPolicyMediaType& aType, const dom::Document& aDoc); +}; + +/** + * This class contains helper funtions which could be used in AutoplayPolicy + * for determing Telemetry use-only result. They shouldn't represent the final + * result of blocking autoplay. + */ +class AutoplayPolicyTelemetryUtils { + public: + // Returns true if a given AudioContext would be allowed to play + // if block autoplay was enabled. If this returns false, it means we would + // either block or ask for permission. + // Note: this is for telemetry purposes, and doesn't check the prefs + // which enable/disable block autoplay. Do not use for blocking logic! + static bool WouldBeAllowedToPlayIfAutoplayDisabled( + const dom::AudioContext& aContext); +}; + +} // namespace mozilla::media + +#endif diff --git a/dom/media/autoplay/GVAutoplayPermissionRequest.cpp b/dom/media/autoplay/GVAutoplayPermissionRequest.cpp new file mode 100644 index 0000000000..600c7d291e --- /dev/null +++ b/dom/media/autoplay/GVAutoplayPermissionRequest.cpp @@ -0,0 +1,236 @@ +/* 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 "GVAutoplayPermissionRequest.h" + +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/Logging.h" +#include "mozilla/StaticPrefs_media.h" +#include "nsGlobalWindowInner.h" + +mozilla::LazyLogModule gGVAutoplayRequestLog("GVAutoplay"); + +namespace mozilla::dom { + +using RType = GVAutoplayRequestType; +using RStatus = GVAutoplayRequestStatus; + +const char* ToGVRequestTypeStr(RType aType) { + switch (aType) { + case RType::eINAUDIBLE: + return "inaudible"; + case RType::eAUDIBLE: + return "audible"; + default: + MOZ_ASSERT_UNREACHABLE("Invalid request type."); + return "invalid"; + } +} + +const char* ToGVRequestStatusStr(RStatus aStatus) { + switch (aStatus) { + case RStatus::eUNKNOWN: + return "Unknown"; + case RStatus::eALLOWED: + return "Allowed"; + case RStatus::eDENIED: + return "Denied"; + case RStatus::ePENDING: + return "Pending"; + default: + MOZ_ASSERT_UNREACHABLE("Invalid status."); + return "Invalid"; + } +} + +// avoid redefined macro in unified build +#undef REQUEST_LOG +#define REQUEST_LOG(msg, ...) \ + if (MOZ_LOG_TEST(gGVAutoplayRequestLog, mozilla::LogLevel::Debug)) { \ + MOZ_LOG(gGVAutoplayRequestLog, LogLevel::Debug, \ + ("Request=%p, Type=%s, " msg, this, \ + ToGVRequestTypeStr(this->mType), ##__VA_ARGS__)); \ + } + +#undef LOG +#define LOG(msg, ...) \ + MOZ_LOG(gGVAutoplayRequestLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +static RStatus GetRequestStatus(BrowsingContext* aContext, RType aType) { + MOZ_ASSERT(aContext); + AssertIsOnMainThread(); + return aType == RType::eAUDIBLE + ? aContext->GetGVAudibleAutoplayRequestStatus() + : aContext->GetGVInaudibleAutoplayRequestStatus(); +} + +// This is copied from the value of `media.geckoview.autoplay.request.testing`. +enum class TestRequest : uint32_t { + ePromptAsNormal = 0, + eAllowAll = 1, + eDenyAll = 2, + eAllowAudible = 3, + eDenyAudible = 4, + eAllowInAudible = 5, + eDenyInAudible = 6, + eLeaveAllPending = 7, +}; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(GVAutoplayPermissionRequest, + ContentPermissionRequestBase) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(GVAutoplayPermissionRequest, + ContentPermissionRequestBase) + +/* static */ +void GVAutoplayPermissionRequest::CreateRequest(nsGlobalWindowInner* aWindow, + BrowsingContext* aContext, + GVAutoplayRequestType aType) { + RefPtr request = + new GVAutoplayPermissionRequest(aWindow, aContext, aType); + request->SetRequestStatus(RStatus::ePENDING); + const TestRequest testingPref = static_cast( + StaticPrefs::media_geckoview_autoplay_request_testing()); + if (testingPref != TestRequest::ePromptAsNormal) { + LOG("Create testing request, tesing value=%u", + static_cast(testingPref)); + if (testingPref == TestRequest::eAllowAll || + (testingPref == TestRequest::eAllowAudible && + aType == RType::eAUDIBLE) || + (testingPref == TestRequest::eAllowInAudible && + aType == RType::eINAUDIBLE)) { + request->Allow(JS::UndefinedHandleValue); + } else if (testingPref == TestRequest::eDenyAll || + (testingPref == TestRequest::eDenyAudible && + aType == RType::eAUDIBLE) || + (testingPref == TestRequest::eDenyInAudible && + aType == RType::eINAUDIBLE)) { + request->Cancel(); + } + } else { + LOG("Dispatch async request"); + request->RequestDelayedTask( + aWindow->EventTargetFor(TaskCategory::Other), + GVAutoplayPermissionRequest::DelayedTaskType::Request); + } +} + +GVAutoplayPermissionRequest::GVAutoplayPermissionRequest( + nsGlobalWindowInner* aWindow, BrowsingContext* aContext, RType aType) + : ContentPermissionRequestBase(aWindow->GetPrincipal(), aWindow, + ""_ns, // No testing pref used in this class + aType == RType::eAUDIBLE + ? "autoplay-media-audible"_ns + : "autoplay-media-inaudible"_ns), + mType(aType), + mContext(aContext) { + MOZ_ASSERT(mContext); + REQUEST_LOG("Request created"); +} + +GVAutoplayPermissionRequest::~GVAutoplayPermissionRequest() { + REQUEST_LOG("Request destroyed"); + // If user doesn't response to the request before it gets destroyed (ex. + // request dismissed, tab closed, naviagation to a new page), then we should + // treat it as a denial. + if (mContext) { + Cancel(); + } +} + +void GVAutoplayPermissionRequest::SetRequestStatus(RStatus aStatus) { + REQUEST_LOG("SetRequestStatus, new status=%s", ToGVRequestStatusStr(aStatus)); + MOZ_ASSERT(mContext); + AssertIsOnMainThread(); + if (mType == RType::eAUDIBLE) { + // Return value of setting synced field should be checked. See bug 1656492. + Unused << mContext->SetGVAudibleAutoplayRequestStatus(aStatus); + } else { + // Return value of setting synced field should be checked. See bug 1656492. + Unused << mContext->SetGVInaudibleAutoplayRequestStatus(aStatus); + } +} + +NS_IMETHODIMP +GVAutoplayPermissionRequest::Cancel() { + MOZ_ASSERT(mContext, "Do not call 'Cancel()' twice!"); + // As the process of replying of the request is an async task, the status + // might have be reset at the time we get the result from parent process. + // Ex. if the page got closed or naviagated immediately after user replied to + // the request. Therefore, the status should be either `pending` or `unknown`. + const RStatus status = GetRequestStatus(mContext, mType); + REQUEST_LOG("Cancel, current status=%s", ToGVRequestStatusStr(status)); + MOZ_ASSERT(status == RStatus::ePENDING || status == RStatus::eUNKNOWN); + if ((status == RStatus::ePENDING) && !mContext->IsDiscarded()) { + SetRequestStatus(RStatus::eDENIED); + } + mContext = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +GVAutoplayPermissionRequest::Allow(JS::Handle aChoices) { + MOZ_ASSERT(mContext, "Do not call 'Allow()' twice!"); + // As the process of replying of the request is an async task, the status + // might have be reset at the time we get the result from parent process. + // Ex. if the page got closed or naviagated immediately after user replied to + // the request. Therefore, the status should be either `pending` or `unknown`. + const RStatus status = GetRequestStatus(mContext, mType); + REQUEST_LOG("Allow, current status=%s", ToGVRequestStatusStr(status)); + MOZ_ASSERT(status == RStatus::ePENDING || status == RStatus::eUNKNOWN); + if (status == RStatus::ePENDING) { + SetRequestStatus(RStatus::eALLOWED); + } + mContext = nullptr; + return NS_OK; +} + +/* static */ +void GVAutoplayPermissionRequestor::AskForPermissionIfNeeded( + nsPIDOMWindowInner* aWindow) { + LOG("Requestor, AskForPermissionIfNeeded"); + if (!aWindow) { + return; + } + + // The request is used for content permission, so it's no need to create a + // content request in parent process if we're in e10s. + if (XRE_IsE10sParentProcess()) { + return; + } + + if (!StaticPrefs::media_geckoview_autoplay_request()) { + return; + } + + LOG("Requestor, check status to decide if we need to create the new request"); + // The request status is stored in top-level browsing context only. + RefPtr context = aWindow->GetBrowsingContext()->Top(); + if (!HasEverAskForRequest(context, RType::eAUDIBLE)) { + CreateAsyncRequest(aWindow, context, RType::eAUDIBLE); + } + if (!HasEverAskForRequest(context, RType::eINAUDIBLE)) { + CreateAsyncRequest(aWindow, context, RType::eINAUDIBLE); + } +} + +/* static */ +bool GVAutoplayPermissionRequestor::HasEverAskForRequest( + BrowsingContext* aContext, RType aType) { + return GetRequestStatus(aContext, aType) != RStatus::eUNKNOWN; +} + +/* static */ +void GVAutoplayPermissionRequestor::CreateAsyncRequest( + nsPIDOMWindowInner* aWindow, BrowsingContext* aContext, + GVAutoplayRequestType aType) { + nsGlobalWindowInner* innerWindow = nsGlobalWindowInner::Cast(aWindow); + if (!innerWindow || !innerWindow->GetPrincipal()) { + return; + } + + GVAutoplayPermissionRequest::CreateRequest(innerWindow, aContext, aType); +} + +} // namespace mozilla::dom diff --git a/dom/media/autoplay/GVAutoplayPermissionRequest.h b/dom/media/autoplay/GVAutoplayPermissionRequest.h new file mode 100644 index 0000000000..f432e365c0 --- /dev/null +++ b/dom/media/autoplay/GVAutoplayPermissionRequest.h @@ -0,0 +1,86 @@ +/* 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/. */ + +#ifndef DOM_MEDIA_GVAUTOPLAYPERMISSIONREQUEST_H_ +#define DOM_MEDIA_GVAUTOPLAYPERMISSIONREQUEST_H_ + +#include "GVAutoplayRequestUtils.h" +#include "nsContentPermissionHelper.h" + +class nsGlobalWindowInner; + +namespace mozilla::dom { + +/** + * This class is used to provide an ability for GeckoView (GV) to allow its + * embedder (application) to decide whether the autoplay media should be allowed + * or denied on the page. We have two types of request, one for audible media, + * another one for inaudible media. Each page would at most have one request per + * type at a time, and the result of request would be effective on that page + * until the page gets reloaded or closed. + */ +class GVAutoplayPermissionRequest : public ContentPermissionRequestBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(GVAutoplayPermissionRequest, + ContentPermissionRequestBase) + + // nsIContentPermissionRequest methods + NS_IMETHOD Cancel(void) override; + NS_IMETHOD Allow(JS::Handle choices) override; + + private: + // Only allow to create this request from the requestor. + friend class GVAutoplayPermissionRequestor; + static void CreateRequest(nsGlobalWindowInner* aWindow, + BrowsingContext* aContext, + GVAutoplayRequestType aType); + + GVAutoplayPermissionRequest(nsGlobalWindowInner* aWindow, + BrowsingContext* aContext, + GVAutoplayRequestType aType); + ~GVAutoplayPermissionRequest(); + + void SetRequestStatus(GVAutoplayRequestStatus aStatus); + + GVAutoplayRequestType mType; + RefPtr mContext; +}; + +/** + * This class provides a method to request autoplay permission for a page, which + * would be used to be a factor to determine if media is allowed to autoplay or + * not on GeckoView. + * + * A page could only have at most one audible request and one inaudible request, + * and once a page has been closed or reloaded, those requests would be dropped. + * In order to achieve that all media existing in the same page can share the + * result of those requests, the request status would only be stored in the + * top-level browsing context, which allows them to be synchronized among + * different processes when Fission is enabled. + * + * The current way we choose is to request for a permission when creating media + * element, in order to get the response from the embedding app before media + * starts playing if the app can response the request quickly enough. However, + * the request might be pending if the app doesn't response to it, we might + * never get the response. As that is just one factor of determining the + * autoplay result, even if we don't get the response for the request, we still + * have a chance to play media. Check AutoplayPolicy to see more details about + * how we decide the final autoplay decision. + */ +class GVAutoplayPermissionRequestor final { + public: + static void AskForPermissionIfNeeded(nsPIDOMWindowInner* aWindow); + + private: + static bool HasEverAskForRequest(BrowsingContext* aContext, + GVAutoplayRequestType aType); + static void CreateAsyncRequest(nsPIDOMWindowInner* aWindow, + BrowsingContext* aContext, + GVAutoplayRequestType aType); +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/autoplay/GVAutoplayRequestStatusIPC.h b/dom/media/autoplay/GVAutoplayRequestStatusIPC.h new file mode 100644 index 0000000000..39d1c22700 --- /dev/null +++ b/dom/media/autoplay/GVAutoplayRequestStatusIPC.h @@ -0,0 +1,23 @@ +/* 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/. */ + +#ifndef DOM_MEDIA_GVAUTOPLAYREQUESTSTATUSIPC_H_ +#define DOM_MEDIA_GVAUTOPLAYREQUESTSTATUSIPC_H_ + +#include "ipc/EnumSerializer.h" + +#include "GVAutoplayRequestUtils.h" + +namespace IPC { + +template <> +struct ParamTraits + : public ContiguousEnumSerializerInclusive< + mozilla::dom::GVAutoplayRequestStatus, + mozilla::dom::GVAutoplayRequestStatus::eUNKNOWN, + mozilla::dom::GVAutoplayRequestStatus::ePENDING> {}; + +} // namespace IPC + +#endif // DOM_MEDIA_GVAUTOPLAYREQUESTSTATUSIPC_H_ diff --git a/dom/media/autoplay/GVAutoplayRequestUtils.h b/dom/media/autoplay/GVAutoplayRequestUtils.h new file mode 100644 index 0000000000..8122afa07e --- /dev/null +++ b/dom/media/autoplay/GVAutoplayRequestUtils.h @@ -0,0 +1,25 @@ +/* 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/. */ + +#ifndef DOM_MEDIA_GVAUTOPLAYREQUESTUTILS_H_ +#define DOM_MEDIA_GVAUTOPLAYREQUESTUTILS_H_ + +#include + +namespace mozilla { +namespace dom { + +enum class GVAutoplayRequestType : bool { eINAUDIBLE = false, eAUDIBLE = true }; + +enum class GVAutoplayRequestStatus : uint32_t { + eUNKNOWN, + eALLOWED, + eDENIED, + ePENDING, +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/media/autoplay/moz.build b/dom/media/autoplay/moz.build new file mode 100644 index 0000000000..338f0798f8 --- /dev/null +++ b/dom/media/autoplay/moz.build @@ -0,0 +1,32 @@ +# vim: set filetype=python: +# 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/. + +with Files("test/**"): + BUG_COMPONENT = ("Core", "Audio/Video: Playback") + +EXPORTS += [ + "AutoplayPolicy.h", + "GVAutoplayPermissionRequest.h", + "GVAutoplayRequestStatusIPC.h", + "GVAutoplayRequestUtils.h", +] + +UNIFIED_SOURCES += [ + "AutoplayPolicy.cpp", + "GVAutoplayPermissionRequest.cpp", +] + +XPIDL_MODULE = "autoplay" +XPIDL_SOURCES += [ + "nsIAutoplay.idl", +] + +MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.ini"] + +BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/media/autoplay/nsIAutoplay.idl b/dom/media/autoplay/nsIAutoplay.idl new file mode 100644 index 0000000000..f18c72ada9 --- /dev/null +++ b/dom/media/autoplay/nsIAutoplay.idl @@ -0,0 +1,17 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsISupports.idl" + +[scriptable, uuid(048a24f6-c4d6-47bc-bea2-f6038d1db80a)] +interface nsIAutoplay : nsISupports +{ + /* + * Possible values for the "media.autoplay.default" preference. + */ + const uint32_t ALLOWED = 0; + const uint32_t BLOCKED = 1; + const uint32_t BLOCKED_ALL = 5; +}; diff --git a/dom/media/autoplay/test/browser/audio.ogg b/dom/media/autoplay/test/browser/audio.ogg new file mode 100644 index 0000000000..7f1833508a Binary files /dev/null and b/dom/media/autoplay/test/browser/audio.ogg differ diff --git a/dom/media/autoplay/test/browser/browser.ini b/dom/media/autoplay/test/browser/browser.ini new file mode 100644 index 0000000000..8bdf83859e --- /dev/null +++ b/dom/media/autoplay/test/browser/browser.ini @@ -0,0 +1,27 @@ +[DEFAULT] +subsuite = media-bc +skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573 +tags = autoplay +support-files = + ../../../test/gizmo.mp4 + audio.ogg + file_empty.html + file_mediaplayback_frame.html + file_nonAutoplayAudio.html + file_video.html + head.js + +[browser_autoplay_policy_detection_global_sticky.js] +[browser_autoplay_policy_detection_global_and_site_sticky.js] +[browser_autoplay_policy_detection_click_to_play.js] +[browser_autoplay_policy_play_twice.js] +[browser_autoplay_policy_user_gestures.js] +https_first_disabled = true +[browser_autoplay_policy_request_permission.js] +https_first_disabled = true +[browser_autoplay_policy_touchScroll.js] +https_first_disabled = true +[browser_autoplay_policy_web_audio.js] +[browser_autoplay_policy_web_audio_with_gum.js] +[browser_autoplay_policy_webRTC_permission.js] +[browser_autoplay_videoDocument.js] diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_click_to_play.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_click_to_play.js new file mode 100644 index 0000000000..576f01b1cf --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_click_to_play.js @@ -0,0 +1,120 @@ +/** + * This test will check the Autoplay Policy Detection API for click-to-play + * blocking policy (media.autoplay.blocking_policy=2) and the blocked value set + * to BLOCKED (block audible) and BLOCKED_ALL (block audible & inaudible). + * + * We will create two video elements in the test page, and then click one of + * them. After doing that, only the element has been clicked can be allowed to + * autoplay, other elements should remain blocked depend on the default blocking + * value. + */ +"use strict"; + +// TODO : remove this when it's enabled by default in bug 1812189. +add_setup(async function setSharedPrefs() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.media.autoplay-policy-detection.enabled", true]], + }); +}); + +async function testAutoplayPolicy(defaultPolicy) { + await setupTestPref(defaultPolicy); + let tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "about:blank" + ); + await createVideoElements(tab); + await SpecialPowers.spawn( + tab.linkedBrowser, + [defaultPolicy], + defaultPolicy => { + is( + content.navigator.getAutoplayPolicy("mediaelement"), + defaultPolicy, + "Check autoplay policy by media element type is correct" + ); + let videos = content.document.getElementsByTagName("video"); + for (let video of videos) { + is( + content.navigator.getAutoplayPolicy(video), + defaultPolicy, + "Check autoplay policy by element is correct" + ); + } + } + ); + + info("click on one video to make it play"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#will-be-clicked", + { button: 0 }, + tab.linkedBrowser + ); + + info("only the element has been clicked can be allowed to autoplay"); + await SpecialPowers.spawn( + tab.linkedBrowser, + [defaultPolicy], + defaultPolicy => { + is( + content.navigator.getAutoplayPolicy("mediaelement"), + defaultPolicy, + "Check autoplay policy by media element type is correct" + ); + let videos = content.document.getElementsByTagName("video"); + for (let video of videos) { + is( + content.navigator.getAutoplayPolicy(video), + video.id === "will-be-clicked" ? "allowed" : defaultPolicy, + "Check autoplay policy by element is correct" + ); + } + } + ); + + BrowserTestUtils.removeTab(tab); +} + +add_task(async function testAutoplayPolicyDetectionForClickToPlay() { + await testAutoplayPolicy("allowed-muted"); + await testAutoplayPolicy("disallowed"); +}); + +// Following are helper functions +async function setupTestPref(defaultPolicy) { + function policyToBlockedValue(defaultPolicy) { + // Value for media.autoplay.default + if (defaultPolicy === "allowed") { + return 0 /* Allowed */; + } else if (defaultPolicy === "allowed-muted") { + return 1 /* Blocked */; + } + return 5 /* Blocked All */; + } + const defaultBlocked = policyToBlockedValue(defaultPolicy); + info(`Set 'media.autoplay.default' to ${defaultBlocked}`); + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.autoplay.default", defaultBlocked], + ["media.autoplay.blocking_policy", 2 /* click-to-play */], + ], + }); +} + +function createVideoElements(tab) { + info("create two video elements in the page"); + let url = GetTestWebBasedURL("gizmo.mp4"); + return SpecialPowers.spawn(tab.linkedBrowser, [url], url => { + let video1 = content.document.createElement("video"); + video1.id = "will-be-clicked"; + video1.controls = true; + video1.src = url; + + let video2 = content.document.createElement("video"); + video2.controls = true; + video2.src = url; + + content.document.body.appendChild(video1); + content.document.body.appendChild(video2); + }); +} diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_and_site_sticky.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_and_site_sticky.js new file mode 100644 index 0000000000..828b3e0986 --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_and_site_sticky.js @@ -0,0 +1,168 @@ +/** + * This test checks whether Autoplay Policy Detection API works correctly under + * different situations of having global permission set for block autoplay + * along with different site permission setting. This test only checks the + * sticky user gesture blocking model. + */ +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +// We can't set site permission on 'about:blank' so we use an empty page. +const PAGE_URL = GetTestWebBasedURL("file_empty.html"); + +add_setup(async function setSharedPrefs() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.media.autoplay-policy-detection.enabled", true], + ["media.autoplay.blocking_policy", 0], + ["media.autoplay.block-webaudio", true], + ], + }); +}); + +add_task(async function testGlobalPermissionIsAllowed() { + await SpecialPowers.pushPrefEnv({ + set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.ALLOWED]], + }); + let tab = await createTabAndSetupPolicyAssertFunc(PAGE_URL); + PermissionTestUtils.add( + tab.linkedBrowser.currentURI, + "autoplay-media", + Services.perms.DENY_ACTION + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], _ => { + info("site permission blocks audible autoplay"); + content.assertAutoplayPolicy({ + resultForElementType: "allowed-muted", + resultForElement: "allowed-muted", + resultForContextType: "disallowed", + resultForContext: "disallowed", + }); + }); + PermissionTestUtils.add( + tab.linkedBrowser.currentURI, + "autoplay-media", + Ci.nsIAutoplay.BLOCKED_ALL + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], _ => { + info("site permission blocks all autoplay"); + content.assertAutoplayPolicy({ + resultForElementType: "disallowed", + resultForElement: "disallowed", + resultForContextType: "disallowed", + resultForContext: "disallowed", + }); + + info( + "activate document by using user gesture, all autoplay will be allowed" + ); + content.document.notifyUserGestureActivation(); + content.assertAutoplayPolicy({ + resultForElementType: "allowed", + resultForElement: "allowed", + resultForContextType: "allowed", + resultForContext: "allowed", + }); + }); + PermissionTestUtils.remove(tab.linkedBrowser.currentURI, "autoplay-media"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testGlobalPermissionIsBlocked() { + await SpecialPowers.pushPrefEnv({ + set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED]], + }); + let tab = await createTabAndSetupPolicyAssertFunc(PAGE_URL); + PermissionTestUtils.add( + tab.linkedBrowser.currentURI, + "autoplay-media", + Services.perms.ALLOW_ACTION + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], _ => { + info("site permission allows all autoplay"); + content.assertAutoplayPolicy({ + resultForElementType: "allowed", + resultForElement: "allowed", + resultForContextType: "allowed", + resultForContext: "allowed", + }); + }); + PermissionTestUtils.add( + tab.linkedBrowser.currentURI, + "autoplay-media", + Ci.nsIAutoplay.BLOCKED_ALL + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], _ => { + info("site permission blocks all autoplay"); + content.assertAutoplayPolicy({ + resultForElementType: "disallowed", + resultForElement: "disallowed", + resultForContextType: "disallowed", + resultForContext: "disallowed", + }); + + info( + "activate document by using user gesture, all autoplay will be allowed" + ); + content.document.notifyUserGestureActivation(); + content.assertAutoplayPolicy({ + resultForElementType: "allowed", + resultForElement: "allowed", + resultForContextType: "allowed", + resultForContext: "allowed", + }); + }); + PermissionTestUtils.remove(tab.linkedBrowser.currentURI, "autoplay-media"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testGlobalPermissionIsBlockedAll() { + await SpecialPowers.pushPrefEnv({ + set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED_ALL]], + }); + let tab = await createTabAndSetupPolicyAssertFunc(PAGE_URL); + PermissionTestUtils.add( + tab.linkedBrowser.currentURI, + "autoplay-media", + Services.perms.ALLOW_ACTION + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], _ => { + info("site permission allows all autoplay"); + content.assertAutoplayPolicy({ + resultForElementType: "allowed", + resultForElement: "allowed", + resultForContextType: "allowed", + resultForContext: "allowed", + }); + }); + PermissionTestUtils.add( + tab.linkedBrowser.currentURI, + "autoplay-media", + Services.perms.DENY_ACTION + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], _ => { + info("site permission blocks audible autoplay"); + content.assertAutoplayPolicy({ + resultForElementType: "allowed-muted", + resultForElement: "allowed-muted", + resultForContextType: "disallowed", + resultForContext: "disallowed", + }); + + info( + "activate document by using user gesture, all autoplay will be allowed" + ); + content.document.notifyUserGestureActivation(); + content.assertAutoplayPolicy({ + resultForElementType: "allowed", + resultForElement: "allowed", + resultForContextType: "allowed", + resultForContext: "allowed", + }); + }); + PermissionTestUtils.remove(tab.linkedBrowser.currentURI, "autoplay-media"); + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_sticky.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_sticky.js new file mode 100644 index 0000000000..ab9fe6b418 --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_sticky.js @@ -0,0 +1,105 @@ +/** + * This test checks whether Autoplay Policy Detection API works correctly under + * different situations of having global permission set for block autoplay. + * This test only checks the sticky user gesture blocking model. + */ +"use strict"; + +add_setup(async function setSharedPrefs() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.media.autoplay-policy-detection.enabled", true], + ["media.autoplay.blocking_policy", 0], + ["media.autoplay.block-webaudio", true], + ], + }); +}); + +add_task(async function testGlobalPermissionIsAllowed() { + await SpecialPowers.pushPrefEnv({ + set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.ALLOWED]], + }); + let tab = await createTabAndSetupPolicyAssertFunc("about:blank"); + await SpecialPowers.spawn(tab.linkedBrowser, [], _ => { + info("global setting allows any autoplay"); + content.assertAutoplayPolicy({ + resultForElementType: "allowed", + resultForElement: "allowed", + resultForContextType: "allowed", + resultForContext: "allowed", + }); + }); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testGlobalPermissionIsBlocked() { + await SpecialPowers.pushPrefEnv({ + set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED]], + }); + let tab = await createTabAndSetupPolicyAssertFunc("about:blank"); + await SpecialPowers.spawn(tab.linkedBrowser, [], _ => { + info( + "global setting allows inaudible autoplay but audible autoplay is still not allowed" + ); + content.assertAutoplayPolicy({ + resultForElementType: "allowed-muted", + resultForElement: "allowed-muted", + resultForContextType: "disallowed", + resultForContext: "disallowed", + }); + + info("tweaking video's muted attribute won't change the result"); + content.video.muted = true; + is( + "allowed-muted", + content.navigator.getAutoplayPolicy(content.video), + "getAutoplayPolicy(video) returns correct value" + ); + content.video.muted = false; + is( + "allowed-muted", + content.navigator.getAutoplayPolicy(content.video), + "getAutoplayPolicy(video) returns correct value" + ); + + info( + "activate document by using user gesture, all autoplay will be allowed" + ); + content.document.notifyUserGestureActivation(); + content.assertAutoplayPolicy({ + resultForElementType: "allowed", + resultForElement: "allowed", + resultForContextType: "allowed", + resultForContext: "allowed", + }); + }); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testGlobalPermissionIsBlockedAll() { + await SpecialPowers.pushPrefEnv({ + set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED_ALL]], + }); + let tab = await createTabAndSetupPolicyAssertFunc("about:blank"); + await SpecialPowers.spawn(tab.linkedBrowser, [], _ => { + info("global setting doesn't allow any autoplay"); + content.assertAutoplayPolicy({ + resultForElementType: "disallowed", + resultForElement: "disallowed", + resultForContextType: "disallowed", + resultForContext: "disallowed", + }); + + info( + "activate document by using user gesture, all autoplay will be allowed" + ); + content.document.notifyUserGestureActivation(); + content.assertAutoplayPolicy({ + resultForElementType: "allowed", + resultForElement: "allowed", + resultForContextType: "allowed", + resultForContext: "allowed", + }); + }); + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_play_twice.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_play_twice.js new file mode 100644 index 0000000000..7130e6e781 --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_play_twice.js @@ -0,0 +1,54 @@ +const VIDEO_PAGE = GetTestWebBasedURL("file_video.html"); + +function setup_test_preference(enableUserGesture) { + let state = enableUserGesture ? "enable" : "disable"; + info(`- set pref : ${state} user gesture -`); + return SpecialPowers.pushPrefEnv({ + set: [ + ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", enableUserGesture ? 0 : 1], + ], + }); +} + +async function allow_play_for_played_video() { + info("- open new tab -"); + let tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "about:blank" + ); + BrowserTestUtils.loadURIString(tab.linkedBrowser, VIDEO_PAGE); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info("- simulate user-click to start video -"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#v", + { button: 0 }, + tab.linkedBrowser + ); + + async function play_video_again() { + let video = content.document.getElementById("v"); + ok(!video.paused, "video is playing"); + + info("- call video play() again -"); + try { + await video.play(); + ok(true, "success to resolve play promise"); + } catch (e) { + ok(false, "promise should not be rejected"); + } + } + await SpecialPowers.spawn(tab.linkedBrowser, [], play_video_again); + + info("- remove tab -"); + BrowserTestUtils.removeTab(tab); +} + +add_task(async function start_test() { + await setup_test_preference(true); + await allow_play_for_played_video(); + + await setup_test_preference(false); + await allow_play_for_played_video(); +}); diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_request_permission.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_request_permission.js new file mode 100644 index 0000000000..5cdb3ffe6c --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_request_permission.js @@ -0,0 +1,269 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +const VIDEO_PAGE_URI = GetTestWebBasedURL("file_empty.html"); +const SAME_ORIGIN_FRAME_URI = GetTestWebBasedURL( + "file_mediaplayback_frame.html" +); +const DIFFERENT_ORIGIN_FRAME_URI = GetTestWebBasedURL( + "file_mediaplayback_frame.html", + { crossOrigin: true } +); + +const gPermissionName = "autoplay-media"; + +function setTestingPreferences(defaultSetting) { + info(`set default autoplay setting to '${defaultSetting}'`); + let defaultValue = + defaultSetting == "blocked" + ? SpecialPowers.Ci.nsIAutoplay.BLOCKED + : SpecialPowers.Ci.nsIAutoplay.ALLOWED; + return SpecialPowers.pushPrefEnv({ + set: [ + ["media.autoplay.default", defaultValue], + ["media.autoplay.blocking_policy", 0], + ["media.autoplay.block-event.enabled", true], + ], + }); +} + +async function testAutoplayExistingPermission(args) { + info("- Starting '" + args.name + "' -"); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: VIDEO_PAGE_URI, + }, + async browser => { + let promptShowing = () => + PopupNotifications.getNotification("autoplay-media", browser); + + PermissionTestUtils.add( + browser.currentURI, + "autoplay-media", + args.permission + ); + ok(!promptShowing(), "Should not be showing permission prompt yet"); + + await loadAutoplayVideo(browser, args); + await checkVideoDidPlay(browser, args); + + // Reset permission. + PermissionTestUtils.remove(browser.currentURI, "autoplay-media"); + + info("- Finished '" + args.name + "' -"); + } + ); +} + +async function testAutoplayExistingPermissionAgainstDefaultSetting(args) { + await setTestingPreferences(args.defaultSetting); + await testAutoplayExistingPermission(args); +} + +// Test the simple ALLOW/BLOCK cases; when permission is already set to ALLOW, +// we shoud be able to autoplay via calling play(), or via the autoplay attribute, +// and when it's set to BLOCK, we should not. +add_task(async () => { + await setTestingPreferences("blocked" /* default setting */); + await testAutoplayExistingPermission({ + name: "Prexisting allow permission autoplay attribute", + permission: Services.perms.ALLOW_ACTION, + shouldPlay: true, + mode: "autoplay attribute", + }); + await testAutoplayExistingPermission({ + name: "Prexisting allow permission call play", + permission: Services.perms.ALLOW_ACTION, + shouldPlay: true, + mode: "call play", + }); + await testAutoplayExistingPermission({ + name: "Prexisting block permission autoplay attribute", + permission: Services.perms.DENY_ACTION, + shouldPlay: false, + mode: "autoplay attribute", + }); + await testAutoplayExistingPermission({ + name: "Prexisting block permission call play", + permission: Services.perms.DENY_ACTION, + shouldPlay: false, + mode: "call play", + }); +}); + +/** + * These tests are used to ensure the autoplay setting for specific site can + * always override the default autoplay setting. + */ +add_task(async () => { + await testAutoplayExistingPermissionAgainstDefaultSetting({ + name: "Site has prexisting allow permission but default setting is 'blocked'", + permission: Services.perms.ALLOW_ACTION, + defaultSetting: "blocked", + shouldPlay: true, + mode: "autoplay attribute", + }); + await testAutoplayExistingPermissionAgainstDefaultSetting({ + name: "Site has prexisting block permission but default setting is 'allowed'", + permission: Services.perms.DENY_ACTION, + defaultSetting: "allowed", + shouldPlay: false, + mode: "autoplay attribute", + }); +}); + +/** + * The permission of the main page's domain would determine the final autoplay + * result when a page contains multiple iframes which are in the different + * domain from the main pages's. + * That means we would not check the permission of iframe's domain, even if it + * has been set. + */ +add_task(async function testExistingPermissionForIframe() { + await setTestingPreferences("blocked" /* default setting */); + await testAutoplayExistingPermissionForIframe({ + name: "Prexisting ALLOW for main page with same origin iframe", + permissionForParent: Services.perms.ALLOW_ACTION, + isIframeDifferentOrgin: true, + shouldPlay: true, + }); + + await testAutoplayExistingPermissionForIframe({ + name: "Prexisting ALLOW for main page with different origin iframe", + permissionForParent: Services.perms.ALLOW_ACTION, + isIframeDifferentOrgin: false, + shouldPlay: true, + }); + + await testAutoplayExistingPermissionForIframe({ + name: "Prexisting ALLOW for main page, prexisting DENY for different origin iframe", + permissionForParent: Services.perms.ALLOW_ACTION, + permissionForChild: Services.perms.DENY_ACTION, + isIframeDifferentOrgin: false, + shouldPlay: true, + }); + + await testAutoplayExistingPermissionForIframe({ + name: "Prexisting DENY for main page, prexisting ALLOW for different origin iframe", + permissionForParent: Services.perms.DENY_ACTION, + permissionForChild: Services.perms.ALLOW_ACTION, + isIframeDifferentOrgin: false, + shouldPlay: false, + }); +}); + +/** + * The following are helper functions. + */ +async function testAutoplayExistingPermissionForIframe(args) { + info(`Start test : ${args.name}`); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: VIDEO_PAGE_URI, + }, + async browser => { + setupSitesPermission(browser, args); + + await createIframe(browser, args); + await checkAutplayInIframe(browser, args); + + clearSitesPermission(browser, args); + } + ); + info(`Finish test : ${args.name}`); +} + +function setupSitesPermission( + browser, + { + isIframeDifferentOrgin, + permissionForParent, + permissionForChild = Services.perms.UNKNOWN_ACTION, + } +) { + info(`setupSitesPermission`); + // Set permission for the main page's domain + setPermissionForBrowser(browser, browser.currentURI, permissionForParent); + if (isIframeDifferentOrgin) { + // Set permission for different domain of the iframe + setPermissionForBrowser( + browser, + DIFFERENT_ORIGIN_FRAME_URI, + permissionForChild + ); + } +} + +function clearSitesPermission(browser, { isIframeDifferentOrgin }) { + info(`clearSitesPermission`); + // Clear permission for the main page's domain + setPermissionForBrowser( + browser, + browser.currentURI, + Services.perms.UNKNOWN_ACTION + ); + if (isIframeDifferentOrgin) { + // Clear permission for different domain of the iframe + setPermissionForBrowser( + browser, + DIFFERENT_ORIGIN_FRAME_URI, + Services.perms.UNKNOWN_ACTION + ); + } +} + +function setPermissionForBrowser(browser, uri, permValue) { + const promptShowing = () => + PopupNotifications.getNotification(gPermissionName, browser); + PermissionTestUtils.add(uri, gPermissionName, permValue); + ok(!promptShowing(), "Should not be showing permission prompt yet"); + is( + PermissionTestUtils.testExactPermission(uri, gPermissionName), + permValue, + "Set permission correctly" + ); +} + +function createIframe(browser, { isIframeDifferentOrgin }) { + const iframeURL = isIframeDifferentOrgin + ? DIFFERENT_ORIGIN_FRAME_URI + : SAME_ORIGIN_FRAME_URI; + return SpecialPowers.spawn(browser, [iframeURL], async url => { + info(`Create iframe and wait until it finsihes loading`); + const iframe = content.document.createElement("iframe"); + iframe.src = url; + content.document.body.appendChild(iframe); + await new Promise(r => (iframe.onload = r)); + }); +} + +function checkAutplayInIframe(browser, args) { + return SpecialPowers.spawn(browser, [args], async ({ shouldPlay }) => { + info(`check if media in iframe can start playing`); + const iframe = content.document.getElementsByTagName("iframe")[0]; + if (!iframe) { + ok(false, `can not get the iframe!`); + return; + } + iframe.contentWindow.postMessage("play", "*"); + await new Promise(r => { + content.onmessage = event => { + if (shouldPlay) { + is(event.data, "played", `played media in iframe`); + } else { + is(event.data, "blocked", `blocked media in iframe`); + } + r(); + }; + }); + }); +} diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_touchScroll.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_touchScroll.js new file mode 100644 index 0000000000..fa28bf2943 --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_touchScroll.js @@ -0,0 +1,103 @@ +/** + * This test is used to ensure that touch in point can activate document and + * allow autoplay, but touch scroll can't activate document. + */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +"use strict"; + +const PAGE = GetTestWebBasedURL("file_nonAutoplayAudio.html"); + +function checkMediaPlayingState(isPlaying) { + let audio = content.document.getElementById("testAudio"); + if (!audio) { + ok(false, "can't get the audio element!"); + } + + is(!audio.paused, isPlaying, "media playing state is correct."); +} + +async function callMediaPlay(shouldStartPlaying) { + let audio = content.document.getElementById("testAudio"); + if (!audio) { + ok(false, "can't get the audio element!"); + } + + info(`call media.play().`); + let playPromise = new Promise((resolve, reject) => { + audio.play().then(() => { + audio.isPlayStarted = true; + resolve(); + }); + content.setTimeout(() => { + if (audio.isPlayStarted) { + return; + } + reject(); + }, 3000); + }); + + let isStartPlaying = await playPromise.then( + () => true, + () => false + ); + is( + isStartPlaying, + shouldStartPlaying, + "media is " + (isStartPlaying ? "" : "not ") + "playing." + ); +} + +async function synthesizeTouchScroll(target, browser) { + const offset = 100; + await BrowserTestUtils.synthesizeTouch( + target, + 0, + 0, + { type: "touchstart" }, + browser + ); + await BrowserTestUtils.synthesizeTouch( + target, + offset / 2, + offset / 2, + { type: "touchmove" }, + browser + ); + await BrowserTestUtils.synthesizeTouch( + target, + offset, + offset, + { type: "touchend" }, + browser + ); +} + +add_task(async function setup_test_preference() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0], + ], + }); +}); + +add_task(async function testTouchScroll() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + async browser => { + info(`- media should not start playing -`); + await SpecialPowers.spawn(browser, [false], checkMediaPlayingState); + + info(`- simulate touch scroll which should not activate document -`); + await synthesizeTouchScroll("#testAudio", browser); + await SpecialPowers.spawn(browser, [false], callMediaPlay); + + info(`- simulate touch at a point which should activate document -`); + await BrowserTestUtils.synthesizeTouch("#testAudio", 0, 0, {}, browser); + await SpecialPowers.spawn(browser, [true], callMediaPlay); + } + ); +}); diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_user_gestures.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_user_gestures.js new file mode 100644 index 0000000000..2b9d8c1158 --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_user_gestures.js @@ -0,0 +1,277 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +const VIDEO_PAGE = GetTestWebBasedURL("file_video.html"); + +const UserGestures = { + MOUSE_CLICK: "mouse-click", + MOUSE_MOVE: "mouse-move", + KEYBOARD_PRESS: "keyboard-press", +}; + +const UserGestureTests = [ + { type: UserGestures.MOUSE_CLICK, isActivationGesture: true }, + { type: UserGestures.MOUSE_MOVE, isActivationGesture: false }, + // test different keycode here. printable key, non-printable key and other + // special keys. + { + type: UserGestures.KEYBOARD_PRESS, + isActivationGesture: true, + keyCode: "a", + }, + { + type: UserGestures.KEYBOARD_PRESS, + isActivationGesture: false, + keyCode: "VK_ESCAPE", + }, + { + type: UserGestures.KEYBOARD_PRESS, + isActivationGesture: true, + keyCode: "VK_RETURN", + }, + { + type: UserGestures.KEYBOARD_PRESS, + isActivationGesture: true, + keyCode: "VK_SPACE", + }, +]; + +/** + * This test is used to ensure we would stop blocking autoplay after document + * has been activated by user gestures. We would treat mouse clicking, key board + * pressing (printable keys or carriage return) as valid user gesture input. + */ +add_task(async function startTestUserGestureInput() { + info("- setup test preference -"); + await setupTestPreferences(); + + info("- test play when page doesn't be activated -"); + await testPlayWithoutUserGesture(); + + info("- test play after page got user gesture -"); + for (let idx = 0; idx < UserGestureTests.length; idx++) { + info("- test play after page got user gesture -"); + await testPlayWithUserGesture(UserGestureTests[idx]); + + info("- test web audio with user gesture -"); + await testWebAudioWithUserGesture(UserGestureTests[idx]); + } +}); + +/** + * testing helper functions + */ +function setupTestPreferences() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0], + ["media.autoplay.block-event.enabled", true], + ["media.autoplay.block-webaudio", true], + ], + }); +} + +function simulateUserGesture(gesture, targetBrowser) { + info(`- simulate ${gesture.type} event -`); + switch (gesture.type) { + case UserGestures.MOUSE_CLICK: + return BrowserTestUtils.synthesizeMouseAtCenter( + "body", + { button: 0 }, + targetBrowser + ); + case UserGestures.MOUSE_MOVE: + return BrowserTestUtils.synthesizeMouseAtCenter( + "body", + { type: "mousemove" }, + targetBrowser + ); + case UserGestures.KEYBOARD_PRESS: + info(`- keycode=${gesture.keyCode} -`); + return BrowserTestUtils.synthesizeKey(gesture.keyCode, {}, targetBrowser); + default: + ok(false, "undefined user gesture"); + return false; + } +} + +async function testPlayWithoutUserGesture() { + info("- open new tab -"); + let tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "about:blank" + ); + BrowserTestUtils.loadURIString(tab.linkedBrowser, VIDEO_PAGE); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + async function checkAutoplayKeyword() { + info("- create an new autoplay video -"); + let video = content.document.createElement("video"); + video.src = "gizmo.mp4"; + video.autoplay = true; + let canplayPromise = new Promise(function (resolve) { + video.addEventListener( + "canplaythrough", + function () { + resolve(); + }, + { once: true } + ); + }); + content.document.body.appendChild(video); + + info("- can't autoplay without user activation -"); + await canplayPromise; + ok(video.paused, "video can't start without user input."); + } + await SpecialPowers.spawn(tab.linkedBrowser, [], checkAutoplayKeyword); + + async function playVideo() { + let video = content.document.getElementById("v"); + info("- call play() without user activation -"); + await video.play().catch(function () { + ok(video.paused, "video can't start play without user input."); + }); + } + await SpecialPowers.spawn(tab.linkedBrowser, [], playVideo); + + info("- remove tab -"); + BrowserTestUtils.removeTab(tab); +} + +async function testPlayWithUserGesture(gesture) { + info("- open new tab -"); + let tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "about:blank" + ); + BrowserTestUtils.loadURIString(tab.linkedBrowser, VIDEO_PAGE); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info("- simulate user gesture -"); + await simulateUserGesture(gesture, tab.linkedBrowser); + + info("- call play() -"); + async function playVideo(gesture) { + let video = content.document.getElementById("v"); + try { + await video.play(); + ok(gesture.isActivationGesture, "user gesture can activate the page"); + ok(!video.paused, "video starts playing."); + } catch (e) { + ok( + !gesture.isActivationGesture, + "user gesture can not activate the page" + ); + ok(video.paused, "video can not start playing."); + } + } + + await SpecialPowers.spawn(tab.linkedBrowser, [gesture], playVideo); + + info("- remove tab -"); + BrowserTestUtils.removeTab(tab); +} + +function createAudioContext() { + content.ac = new content.AudioContext(); + let ac = content.ac; + ac.resumePromises = []; + ac.stateChangePromise = new Promise(resolve => { + ac.addEventListener( + "statechange", + function () { + resolve(); + }, + { once: true } + ); + }); + ac.notAllowedToStart = new Promise(resolve => { + ac.addEventListener( + "blocked", + function () { + resolve(); + }, + { once: true } + ); + }); +} + +function resumeWithoutExpectedSuccess() { + let ac = content.ac; + let promise = ac.resume(); + ac.resumePromises.push(promise); + return new Promise((resolve, reject) => { + content.setTimeout(() => { + if (ac.state == "suspended") { + ok(true, "audio context is still suspended"); + resolve(); + } else { + reject("audio context should not be allowed to start"); + } + }, 2000); + }); +} + +function resumeWithExpectedSuccess() { + let ac = content.ac; + ac.resumePromises.push(ac.resume()); + return Promise.all(ac.resumePromises).then(() => { + ok(ac.state == "running", "audio context starts running"); + }); +} + +async function testWebAudioWithUserGesture(gesture) { + info("- open new tab -"); + let tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "about:blank" + ); + info("- create audio context -"); + await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + content.ac = new content.AudioContext(); + let ac = content.ac; + ac.resumePromises = []; + return new Promise(resolve => { + ac.addEventListener( + "blocked", + function () { + Assert.equal( + ac.state, + "suspended", + `AudioContext is not started yet.` + ); + resolve(); + }, + { once: true } + ); + }); + }); + + info("- calling resume() -"); + try { + await SpecialPowers.spawn( + tab.linkedBrowser, + [], + resumeWithoutExpectedSuccess + ); + } catch (error) { + ok(false, error.toString()); + } + + info("- simulate user gesture -"); + await simulateUserGesture(gesture, tab.linkedBrowser); + + info("- calling resume() again"); + try { + let resumeFunc = gesture.isActivationGesture + ? resumeWithExpectedSuccess + : resumeWithoutExpectedSuccess; + await SpecialPowers.spawn(tab.linkedBrowser, [], resumeFunc); + } catch (error) { + ok(false, error.toString()); + } + + info("- remove tab -"); + await BrowserTestUtils.removeTab(tab); +} diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_webRTC_permission.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_webRTC_permission.js new file mode 100644 index 0000000000..8afae4d08e --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_webRTC_permission.js @@ -0,0 +1,67 @@ +/** + * This test is used to ensure site which has granted 'camera' or 'microphone' + * or 'screen' permission could be allowed to autoplay. + */ +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +const VIDEO_PAGE = GetTestWebBasedURL("file_empty.html"); + +add_task(() => { + return SpecialPowers.pushPrefEnv({ + set: [ + ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0], + ["media.autoplay.block-event.enabled", true], + ], + }); +}); + +async function testAutoplayWebRTCPermission(args) { + info(`- Starting ${args.name} -`); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: VIDEO_PAGE, + }, + async browser => { + PermissionTestUtils.add( + browser.currentURI, + args.permission, + Services.perms.ALLOW_ACTION + ); + + await loadAutoplayVideo(browser, args); + await checkVideoDidPlay(browser, args); + + // Reset permission. + PermissionTestUtils.remove(browser.currentURI, args.permission); + + info(`- Finished ${args.name} -`); + } + ); +} + +add_task(async function start_test() { + await testAutoplayWebRTCPermission({ + name: "Site with camera permission", + permission: "camera", + shouldPlay: true, + mode: "call play", + }); + await testAutoplayWebRTCPermission({ + name: "Site with microphone permission", + permission: "microphone", + shouldPlay: true, + mode: "call play", + }); + await testAutoplayWebRTCPermission({ + name: "Site with screen permission", + permission: "screen", + shouldPlay: true, + mode: "call play", + }); +}); diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio.js new file mode 100644 index 0000000000..b6e1133dd1 --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio.js @@ -0,0 +1,217 @@ +/** + * This test is used for testing whether WebAudio can be started correctly in + * different scenarios, such as + * 1) site has existing 'autoplay-media' permission for allowing autoplay + * 2) site has existing 'autoplay-media' permission for blocking autoplay + * 3) site doesn't have permission, start audio context by calling resume() or + * AudioScheduledNode.start() after granting user activation. + */ +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +const PAGE = GetTestWebBasedURL("file_empty.html"); + +function setup_test_preference() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0], + ["media.autoplay.block-webaudio", true], + ["media.autoplay.block-event.enabled", true], + ], + }); +} + +function createAudioContext() { + content.ac = new content.AudioContext(); + const ac = content.ac; + + ac.allowedToStart = new Promise(resolve => { + ac.addEventListener( + "statechange", + function () { + if (ac.state === "running") { + resolve(); + } + }, + { once: true } + ); + }); + + ac.notAllowedToStart = new Promise(resolve => { + ac.addEventListener( + "blocked", + function () { + resolve(); + }, + { once: true } + ); + }); +} + +async function checkIfAudioContextIsAllowedToStart(isAllowedToStart) { + const ac = content.ac; + if (isAllowedToStart) { + await ac.allowedToStart; + ok(ac.state === "running", `AudioContext is running.`); + } else { + await ac.notAllowedToStart; + ok(ac.state === "suspended", `AudioContext is not started yet.`); + } +} + +async function resumeAudioContext(isAllowedToStart) { + const ac = content.ac; + const resumePromise = ac.resume(); + const blockedPromise = new Promise(resolve => { + ac.addEventListener( + "blocked", + function () { + resolve(); + }, + { once: true } + ); + }); + + if (isAllowedToStart) { + await resumePromise; + ok(true, `successfully resume AudioContext.`); + } else { + await blockedPromise; + ok(true, `resume is blocked because AudioContext is not allowed to start.`); + } +} + +function startAudioContext(method) { + const ac = content.ac; + if (method == "AudioContext") { + info(`using AudioContext.resume() to start AudioContext`); + ac.resume(); + return; + } + info(`using ${method}.start() to start AudioContext`); + let node; + switch (method) { + case "AudioBufferSourceNode": + node = ac.createBufferSource(); + break; + case "ConstantSourceNode": + node = ac.createConstantSource(); + break; + case "OscillatorNode": + node = ac.createOscillator(); + break; + default: + ok(false, "undefined AudioScheduledSourceNode type"); + return; + } + node.connect(ac.destination); + node.start(); +} + +async function testAutoplayExistingPermission({ name, permission }) { + info(`- starting \"${name}\" -`); + const tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + PAGE + ); + const browser = tab.linkedBrowser; + + info(`- set the 'autoplay-media' permission -`); + const promptShow = () => + PopupNotifications.getNotification("autoplay-media", browser); + PermissionTestUtils.add(browser.currentURI, "autoplay-media", permission); + ok(!promptShow(), `should not be showing permission prompt yet`); + + info(`- create audio context -`); + await SpecialPowers.spawn(browser, [], createAudioContext); + + info(`- check AudioContext status -`); + const isAllowedToStart = permission === Services.perms.ALLOW_ACTION; + await SpecialPowers.spawn( + browser, + [isAllowedToStart], + checkIfAudioContextIsAllowedToStart + ); + await SpecialPowers.spawn(browser, [isAllowedToStart], resumeAudioContext); + + info(`- remove tab -`); + PermissionTestUtils.remove(browser.currentURI, "autoplay-media"); + await BrowserTestUtils.removeTab(tab); +} + +async function testAutoplayUnknownPermission({ name, method }) { + info(`- starting \"${name}\" -`); + const tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + PAGE + ); + const browser = tab.linkedBrowser; + + info(`- set the 'autoplay-media' permission to UNKNOWN -`); + const promptShow = () => + PopupNotifications.getNotification("autoplay-media", browser); + PermissionTestUtils.add( + browser.currentURI, + "autoplay-media", + Services.perms.UNKNOWN_ACTION + ); + ok(!promptShow(), `should not be showing permission prompt yet`); + + info(`- create AudioContext which should not start -`); + await SpecialPowers.spawn(browser, [], createAudioContext); + await SpecialPowers.spawn( + browser, + [false], + checkIfAudioContextIsAllowedToStart + ); + + info(`- simulate user activate the page -`); + await SpecialPowers.spawn(browser, [], () => { + content.document.notifyUserGestureActivation(); + }); + + info(`- try to start AudioContext -`); + await SpecialPowers.spawn(browser, [method], startAudioContext); + + info(`- check AudioContext status -`); + await SpecialPowers.spawn( + browser, + [true], + checkIfAudioContextIsAllowedToStart + ); + await SpecialPowers.spawn(browser, [true], resumeAudioContext); + + info(`- remove tab -`); + PermissionTestUtils.remove(browser.currentURI, "autoplay-media"); + await BrowserTestUtils.removeTab(tab); +} + +add_task(async function start_tests() { + info("- setup test preference -"); + await setup_test_preference(); + + await testAutoplayExistingPermission({ + name: "Prexisting allow permission", + permission: Services.perms.ALLOW_ACTION, + }); + await testAutoplayExistingPermission({ + name: "Prexisting block permission", + permission: Services.perms.DENY_ACTION, + }); + const startMethods = [ + "AudioContext", + "AudioBufferSourceNode", + "ConstantSourceNode", + "OscillatorNode", + ]; + for (let method of startMethods) { + await testAutoplayUnknownPermission({ + name: "Unknown permission and start AudioContext after granting user activation", + method, + }); + } +}); diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio_with_gum.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio_with_gum.js new file mode 100644 index 0000000000..45d5c71a52 --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio_with_gum.js @@ -0,0 +1,174 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +/** + * This test is used to ensure web audio can be allowed to start when we have + * GUM permission. + */ +add_task(async function startTestingWebAudioWithGUM() { + info("- setup test preference -"); + await setupTestPreferences(); + + info("- test web audio with gUM success -"); + await testWebAudioWithGUM({ + constraints: { audio: true }, + shouldAllowStartingContext: true, + }); + await testWebAudioWithGUM({ + constraints: { video: true }, + shouldAllowStartingContext: true, + }); + await testWebAudioWithGUM({ + constraints: { video: true, audio: true }, + shouldAllowStartingContext: true, + }); + + await SpecialPowers.pushPrefEnv({ + set: [["media.navigator.permission.force", true]], + }).then(async function () { + info("- test web audio with gUM denied -"); + await testWebAudioWithGUM({ + constraints: { video: true }, + shouldAllowStartingContext: false, + }); + await testWebAudioWithGUM({ + constraints: { audio: true }, + shouldAllowStartingContext: false, + }); + await testWebAudioWithGUM({ + constraints: { video: true, audio: true }, + shouldAllowStartingContext: false, + }); + }); +}); + +/** + * testing helper functions + */ +function setupTestPreferences() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0], + ["media.autoplay.block-event.enabled", true], + ["media.autoplay.block-webaudio", true], + ["media.navigator.permission.fake", true], + ], + }); +} + +function createAudioContext() { + content.ac = new content.AudioContext(); + let ac = content.ac; + ac.resumePromises = []; + ac.stateChangePromise = new Promise(resolve => { + ac.addEventListener( + "statechange", + function () { + resolve(); + }, + { once: true } + ); + }); + ac.notAllowedToStart = new Promise(resolve => { + ac.addEventListener( + "blocked", + function () { + resolve(); + }, + { once: true } + ); + }); +} + +async function checkingAudioContextRunningState() { + let ac = content.ac; + await ac.notAllowedToStart; + ok(ac.state === "suspended", `AudioContext is not started yet.`); +} + +function resumeWithoutExpectedSuccess() { + let ac = content.ac; + let promise = ac.resume(); + ac.resumePromises.push(promise); + return new Promise((resolve, reject) => { + content.setTimeout(() => { + if (ac.state == "suspended") { + ok(true, "audio context is still suspended"); + resolve(); + } else { + reject("audio context should not be allowed to start"); + } + }, 2000); + }); +} + +function resumeWithExpectedSuccess() { + let ac = content.ac; + ac.resumePromises.push(ac.resume()); + return Promise.all(ac.resumePromises).then(() => { + ok(ac.state == "running", "audio context starts running"); + }); +} + +async function callGUM(testParameters) { + info("- calling gum with " + JSON.stringify(testParameters.constraints)); + if (testParameters.shouldAllowStartingContext) { + // Because of the prefs we've set and passed, this is going to allow the + // window to start an AudioContext synchronously. + testParameters.constraints.fake = true; + await content.navigator.mediaDevices.getUserMedia( + testParameters.constraints + ); + return; + } + + // Call gUM, without sucess: we've made it so that only fake requests + // succeed without permission, and this is requesting non-fake-devices. Return + // a resolved promise so that the test continues, but the getUserMedia Promise + // will never be resolved. + // We do this to check that it's not merely calling gUM that allows starting + // an AudioContext, it's having the Promise it return resolved successfuly, + // because of saved permissions for an origin or explicit user consent using + // the prompt. + content.navigator.mediaDevices.getUserMedia(testParameters.constraints); +} + +async function testWebAudioWithGUM(testParameters) { + info("- open new tab -"); + let tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "https://example.com" + ); + info("- create audio context -"); + await SpecialPowers.spawn(tab.linkedBrowser, [], createAudioContext); + + info("- check whether audio context starts running -"); + try { + await SpecialPowers.spawn( + tab.linkedBrowser, + [], + checkingAudioContextRunningState + ); + } catch (error) { + ok(false, error.toString()); + } + + try { + await SpecialPowers.spawn(tab.linkedBrowser, [testParameters], callGUM); + } catch (error) { + ok(false, error.toString()); + } + + info("- calling resume() again"); + try { + let resumeFunc = testParameters.shouldAllowStartingContext + ? resumeWithExpectedSuccess + : resumeWithoutExpectedSuccess; + await SpecialPowers.spawn(tab.linkedBrowser, [], resumeFunc); + } catch (error) { + ok(false, error.toString()); + } + + info("- remove tab -"); + await BrowserTestUtils.removeTab(tab); +} diff --git a/dom/media/autoplay/test/browser/browser_autoplay_videoDocument.js b/dom/media/autoplay/test/browser/browser_autoplay_videoDocument.js new file mode 100644 index 0000000000..77ce4ddbc1 --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_videoDocument.js @@ -0,0 +1,80 @@ +"use strict"; + +const PAGE = GetTestWebBasedURL("audio.ogg"); + +function setup_test_preference() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0], + ], + }); +} + +async function checkIsVideoDocumentAutoplay(browser) { + const played = await SpecialPowers.spawn(browser, [], async () => { + const video = content.document.getElementsByTagName("video")[0]; + const played = + video && + (await video.play().then( + () => true, + () => false + )); + return played; + }); + ok(played, "Should be able to play in video document."); +} + +async function checkIsIframeVideoDocumentAutoplay(browser) { + info("- create iframe video document -"); + const iframeBC = await SpecialPowers.spawn(browser, [PAGE], async pageURL => { + const iframe = content.document.createElement("iframe"); + iframe.src = pageURL; + content.document.body.appendChild(iframe); + const iframeLoaded = new Promise((resolve, reject) => { + iframe.addEventListener("load", e => resolve(), { once: true }); + }); + await iframeLoaded; + return iframe.browsingContext; + }); + + info("- check whether iframe video document starts playing -"); + const [paused, playedLength] = await SpecialPowers.spawn(iframeBC, [], () => { + const video = content.document.querySelector("video"); + return [video.paused, video.played.length]; + }); + ok(paused, "Subdoc video should not have played"); + is(playedLength, 0, "Should have empty played ranges"); +} + +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + async browser => { + info("- setup test preference -"); + await setup_test_preference(); + + info(`- check whether video document is autoplay -`); + await checkIsVideoDocumentAutoplay(browser); + } + ); +}); + +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:blank", + }, + async browser => { + info("- setup test preference -"); + await setup_test_preference(); + + info(`- check whether video document in iframe is autoplay -`); + await checkIsIframeVideoDocumentAutoplay(browser); + } + ); +}); diff --git a/dom/media/autoplay/test/browser/file_empty.html b/dom/media/autoplay/test/browser/file_empty.html new file mode 100644 index 0000000000..d2b0361f09 --- /dev/null +++ b/dom/media/autoplay/test/browser/file_empty.html @@ -0,0 +1,8 @@ + + + + Page left intentionally blank... + + + + diff --git a/dom/media/autoplay/test/browser/file_mediaplayback_frame.html b/dom/media/autoplay/test/browser/file_mediaplayback_frame.html new file mode 100644 index 0000000000..b5685c07b3 --- /dev/null +++ b/dom/media/autoplay/test/browser/file_mediaplayback_frame.html @@ -0,0 +1,21 @@ + + + +Non-Autoplay page being used in Iframe + + + + + + diff --git a/dom/media/autoplay/test/browser/file_nonAutoplayAudio.html b/dom/media/autoplay/test/browser/file_nonAutoplayAudio.html new file mode 100644 index 0000000000..4d2641021a --- /dev/null +++ b/dom/media/autoplay/test/browser/file_nonAutoplayAudio.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dom/media/autoplay/test/browser/file_video.html b/dom/media/autoplay/test/browser/file_video.html new file mode 100644 index 0000000000..3c70268fbb --- /dev/null +++ b/dom/media/autoplay/test/browser/file_video.html @@ -0,0 +1,9 @@ + + + +video + + + + + diff --git a/dom/media/autoplay/test/browser/head.js b/dom/media/autoplay/test/browser/head.js new file mode 100644 index 0000000000..c84850900a --- /dev/null +++ b/dom/media/autoplay/test/browser/head.js @@ -0,0 +1,149 @@ +/** + * Return a web-based URL for a given file based on the testing directory. + * @param {String} fileName + * file that caller wants its web-based url + * @param {Boolean} crossOrigin [optional] + * if set, then return a url with different origin. The default value is + * false. + */ +function GetTestWebBasedURL(fileName, { crossOrigin = false } = {}) { + const origin = crossOrigin ? "http://example.org" : "http://example.com"; + return ( + getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) + + fileName + ); +} + +/** + * Runs a content script that creates an autoplay video. + * @param {browserElement} browser + * the browser to run the script in + * @param {object} args + * test case definition, required members + * { + * mode: String, "autoplay attribute" or "call play". + * } + */ +function loadAutoplayVideo(browser, args) { + return SpecialPowers.spawn(browser, [args], async args => { + info("- create a new autoplay video -"); + let video = content.document.createElement("video"); + video.id = "v1"; + video.didPlayPromise = new Promise((resolve, reject) => { + video.addEventListener( + "playing", + e => { + video.didPlay = true; + resolve(); + }, + { once: true } + ); + video.addEventListener( + "blocked", + e => { + video.didPlay = false; + resolve(); + }, + { once: true } + ); + }); + if (args.mode == "autoplay attribute") { + info("autoplay attribute set to true"); + video.autoplay = true; + } else if (args.mode == "call play") { + info("will call play() when reached loadedmetadata"); + video.addEventListener( + "loadedmetadata", + e => { + video.play().then( + () => { + info("video play() resolved"); + }, + () => { + info("video play() rejected"); + } + ); + }, + { once: true } + ); + } else { + ok(false, "Invalid 'mode' arg"); + } + video.src = "gizmo.mp4"; + content.document.body.appendChild(video); + }); +} + +/** + * Runs a content script that checks whether the video created by + * loadAutoplayVideo() started playing. + * @param {browserElement} browser + * the browser to run the script in + * @param {object} args + * test case definition, required members + * { + * name: String, description of test. + * mode: String, "autoplay attribute" or "call play". + * shouldPlay: boolean, whether video should play. + * } + */ +function checkVideoDidPlay(browser, args) { + return SpecialPowers.spawn(browser, [args], async args => { + let video = content.document.getElementById("v1"); + await video.didPlayPromise; + is( + video.didPlay, + args.shouldPlay, + args.name + + " should " + + (!args.shouldPlay ? "not " : "") + + "be able to autoplay" + ); + video.src = ""; + content.document.body.remove(video); + }); +} + +/** + * Create a tab that will load the given url, and define an autoplay policy + * check function inside the content window in that tab. This function should + * only be used when `dom.media.autoplay-policy-detection.enabled` is true. + * @param {url} url + * the url which the created tab should load + */ +async function createTabAndSetupPolicyAssertFunc(url) { + let tab = await BrowserTestUtils.openNewForegroundTab(window.gBrowser, url); + await SpecialPowers.spawn(tab.linkedBrowser, [], _ => { + content.video = content.document.createElement("video"); + content.ac = new content.AudioContext(); + content.assertAutoplayPolicy = ({ + resultForElementType, + resultForElement, + resultForContextType, + resultForContext, + }) => { + is( + content.navigator.getAutoplayPolicy("mediaelement"), + resultForElementType, + "getAutoplayPolicy('mediaelement') returns correct value" + ); + is( + content.navigator.getAutoplayPolicy(content.video), + resultForElement, + "getAutoplayPolicy(content.video) returns correct value" + ); + // note, per spec "allowed-muted" won't be used for audio context. + is( + content.navigator.getAutoplayPolicy("audiocontext"), + resultForContextType, + "getAutoplayPolicy('audiocontext') returns correct value" + ); + is( + content.navigator.getAutoplayPolicy(content.ac), + resultForContext, + "getAutoplayPolicy(content.ac) returns correct value" + ); + }; + }); + return tab; +} diff --git a/dom/media/autoplay/test/mochitest/AutoplayTestUtils.js b/dom/media/autoplay/test/mochitest/AutoplayTestUtils.js new file mode 100644 index 0000000000..aa8990c9d9 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/AutoplayTestUtils.js @@ -0,0 +1,46 @@ +/* import-globals-from ../../../test/manifest.js */ + +function playAndPostResult(muted, parent_window) { + let element = document.createElement("video"); + element.preload = "auto"; + element.muted = muted; + element.src = "short.mp4"; + element.id = "video"; + document.body.appendChild(element); + element.play().then( + () => { + parent_window.postMessage( + { played: true, allowedToPlay: element.allowedToPlay }, + "*" + ); + }, + () => { + parent_window.postMessage( + { played: false, allowedToPlay: element.allowedToPlay }, + "*" + ); + } + ); +} + +function nextWindowMessage() { + return nextEvent(window, "message"); +} + +function log(msg) { + var log_pane = document.body; + log_pane.appendChild(document.createTextNode(msg)); + log_pane.appendChild(document.createElement("br")); +} + +const autoplayPermission = "autoplay-media"; + +async function pushAutoplayAllowedPermission() { + return SpecialPowers.pushPermissions([ + { + type: autoplayPermission, + allow: true, + context: document, + }, + ]); +} diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_frame.html b/dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_frame.html new file mode 100644 index 0000000000..de5ad1989f --- /dev/null +++ b/dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_frame.html @@ -0,0 +1,24 @@ + + + + GV autoplay play request test + + + + + + + + diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_window.html b/dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_window.html new file mode 100644 index 0000000000..56e4e1031c --- /dev/null +++ b/dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_window.html @@ -0,0 +1,65 @@ + + + + GV autoplay play request test + + + + + + + + + diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_frame.html b/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_frame.html new file mode 100644 index 0000000000..5dfb3da862 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_frame.html @@ -0,0 +1,32 @@ + + + + Autoplay policy frame + + + + + + + + + diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_window.html b/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_window.html new file mode 100644 index 0000000000..60c5a0cec1 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_window.html @@ -0,0 +1,80 @@ + + + + Autoplay policy window + + + + + + + +
+      
+    
+ + diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_policy_eventdown_activation.html b/dom/media/autoplay/test/mochitest/file_autoplay_policy_eventdown_activation.html new file mode 100644 index 0000000000..e25b6401d1 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_eventdown_activation.html @@ -0,0 +1,85 @@ + + + + + Autoplay policy window + + + + + + + + +
+      
+    
+ + + diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html b/dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html new file mode 100644 index 0000000000..c9982f932a --- /dev/null +++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html @@ -0,0 +1,148 @@ + + + + + Autoplay policy window + + + + + + + + +
This is a div with id=x.
+
+      
+      
+    
+ + + diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_policy_play_before_loadedmetadata.html b/dom/media/autoplay/test/mochitest/file_autoplay_policy_play_before_loadedmetadata.html new file mode 100644 index 0000000000..3594d0f236 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_play_before_loadedmetadata.html @@ -0,0 +1,63 @@ + + + + + Autoplay policy window + + + + + + + + +
+      
+    
+ + + diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_policy_unmute_pauses.html b/dom/media/autoplay/test/mochitest/file_autoplay_policy_unmute_pauses.html new file mode 100644 index 0000000000..125ee156b6 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_unmute_pauses.html @@ -0,0 +1,65 @@ + + + + + Autoplay policy window + + + + + + + + +
+      
+    
+ + + diff --git a/dom/media/autoplay/test/mochitest/mochitest.ini b/dom/media/autoplay/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..f25b12e953 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/mochitest.ini @@ -0,0 +1,54 @@ +[DEFAULT] +subsuite = media +tags = autoplay +support-files = + ../../../test/manifest.js + ../../../test/320x240.ogv + ../../../test/bogus.duh + ../../../test/detodos-short.opus + ../../../test/flac-s24.flac + ../../../test/gizmo.mp4 + ../../../test/gizmo.webm + ../../../test/gizmo-noaudio.mp4 + ../../../test/gizmo-noaudio.webm + ../../../test/gizmo-short.mp4 + ../../../test/r11025_s16_c1-short.wav + ../../../test/sample.3g2 + ../../../test/sample.3gp + ../../../test/short.mp4 + ../../../test/seek-short.webm + ../../../test/small-shot.flac + ../../../test/small-shot.m4a + ../../../test/small-shot.mp3 + ../../../test/small-shot-mp3.mp4 + ../../../test/small-shot.ogg + ../../../test/vp9-short.webm + AutoplayTestUtils.js + file_autoplay_gv_play_request_frame.html + file_autoplay_gv_play_request_window.html + file_autoplay_policy_activation_frame.html + file_autoplay_policy_activation_window.html + file_autoplay_policy_eventdown_activation.html + file_autoplay_policy_play_before_loadedmetadata.html + file_autoplay_policy_unmute_pauses.html + file_autoplay_policy_key_blacklist.html + +[test_autoplay.html] +[test_autoplay_contentEditable.html] +[test_autoplay_gv_play_request.html] +skip-if = toolkit != 'android' +[test_autoplay_policy.html] +[test_autoplay_policy_activation.html] +[test_autoplay_policy_play_before_loadedmetadata.html] +skip-if = toolkit == 'android' # bug 1591121 +[test_autoplay_policy_eventdown_activation.html] +[test_autoplay_policy_permission.html] +[test_autoplay_policy_unmute_pauses.html] +[test_autoplay_policy_key_blacklist.html] +skip-if = (verify && debug && (os == 'win')) # bug 1424903 +[test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html] +[test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html] +[test_autoplay_policy_web_audio_AudioParamStream.html] +[test_autoplay_policy_web_audio_createMediaStreamSource.html] +[test_streams_autoplay.html] +tags=mtg capturestream diff --git a/dom/media/autoplay/test/mochitest/test_autoplay.html b/dom/media/autoplay/test/mochitest/test_autoplay.html new file mode 100644 index 0000000000..aa936f976d --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay.html @@ -0,0 +1,36 @@ + + + + Media test: autoplay attribute + + + + + + + +
+
+
+ + diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_contentEditable.html b/dom/media/autoplay/test/mochitest/test_autoplay_contentEditable.html new file mode 100644 index 0000000000..0c0ec31797 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_contentEditable.html @@ -0,0 +1,67 @@ + + + + Media test: play() method + + + + + +
+
+
+
+ + diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_gv_play_request.html b/dom/media/autoplay/test/mochitest/test_autoplay_gv_play_request.html new file mode 100644 index 0000000000..760c452592 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_gv_play_request.html @@ -0,0 +1,221 @@ + + + + GV Autoplay policy test + + + + + + + + + diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy.html new file mode 100644 index 0000000000..dae388b21d --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy.html @@ -0,0 +1,174 @@ + + + + + Autoplay policy test + + + + + +
+
+
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_activation.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_activation.html
new file mode 100644
index 0000000000..eae266030e
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_activation.html
@@ -0,0 +1,180 @@
+
+
+  
+    Autoplay policy test
+    
+    
+    
+    
+  
+  
+    
+      
+    
+ + diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_eventdown_activation.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_eventdown_activation.html new file mode 100644 index 0000000000..878f996ec5 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_eventdown_activation.html @@ -0,0 +1,55 @@ + + + + + Autoplay policy test + + + + + + + +
+      
+    
+ + + diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_key_blacklist.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_key_blacklist.html new file mode 100644 index 0000000000..a85c63713a --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_key_blacklist.html @@ -0,0 +1,47 @@ + + + + + Autoplay policy test + + + + + + + +
+      
+    
+ + + diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_permission.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_permission.html new file mode 100644 index 0000000000..b91e4d7be8 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_permission.html @@ -0,0 +1,80 @@ + + + + + Autoplay policy test + + + + + + + +
+      
+    
+ + + diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_play_before_loadedmetadata.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_play_before_loadedmetadata.html new file mode 100644 index 0000000000..b5f70be227 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_play_before_loadedmetadata.html @@ -0,0 +1,73 @@ + + + + + Autoplay policy test + + + + + + + +
+      
+    
+ + + diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_unmute_pauses.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_unmute_pauses.html new file mode 100644 index 0000000000..29ce4b801f --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_unmute_pauses.html @@ -0,0 +1,64 @@ + + + + + Autoplay policy test + + + + + + + +
+      
+    
+ + + diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_AudioParamStream.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_AudioParamStream.html new file mode 100644 index 0000000000..27dfa5388f --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_AudioParamStream.html @@ -0,0 +1,171 @@ + + + + Autoplay policy test : suspend/resume the AudioParam's stream + + + + + + diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_createMediaStreamSource.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_createMediaStreamSource.html new file mode 100644 index 0000000000..5fe9aa64fc --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_createMediaStreamSource.html @@ -0,0 +1,119 @@ + + + + Autoplay policy test : createMediaStreamSource with active stream + + + + + + diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html new file mode 100644 index 0000000000..41fab54133 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html @@ -0,0 +1,105 @@ + + + + Autoplay policy test : use media element as source for web audio + + + + + + diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html new file mode 100644 index 0000000000..46df256391 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html @@ -0,0 +1,96 @@ + + + + Autoplay policy test : do not resume AudioContext which is suspended by page + + + + + + diff --git a/dom/media/autoplay/test/mochitest/test_streams_autoplay.html b/dom/media/autoplay/test/mochitest/test_streams_autoplay.html new file mode 100644 index 0000000000..0b8630a323 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_streams_autoplay.html @@ -0,0 +1,47 @@ + + + + Test that a MediaStream source triggers autoplay + + + + + +
+
+
+ + -- cgit v1.2.3