diff options
Diffstat (limited to 'dom/media/autoplay')
51 files changed, 5066 insertions, 0 deletions
diff --git a/dom/media/autoplay/AutoplayPolicy.cpp b/dom/media/autoplay/AutoplayPolicy.cpp new file mode 100644 index 0000000000..fafa20477e --- /dev/null +++ b/dom/media/autoplay/AutoplayPolicy.cpp @@ -0,0 +1,496 @@ +/* -*- 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_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<BrowsingContext> 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<nsPIDOMWindowInner> 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<nsPIDOMWindowInner> 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<nsIPermissionManager> 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..beb3fb59fc --- /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<GVAutoplayPermissionRequest> request = + new GVAutoplayPermissionRequest(aWindow, aContext, aType); + request->SetRequestStatus(RStatus::ePENDING); + const TestRequest testingPref = static_cast<TestRequest>( + StaticPrefs::media_geckoview_autoplay_request_testing()); + if (testingPref != TestRequest::ePromptAsNormal) { + LOG("Create testing request, tesing value=%u", + static_cast<uint32_t>(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->SerialEventTarget(), + 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<JS::Value> 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<BrowsingContext> 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<JS::Value> 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<BrowsingContext> 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<mozilla::dom::GVAutoplayRequestStatus> + : 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 <cstdint> + +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..46412217b2 --- /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.toml"]
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.toml"]
+
+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 Binary files differnew file mode 100644 index 0000000000..7f1833508a --- /dev/null +++ b/dom/media/autoplay/test/browser/audio.ogg diff --git a/dom/media/autoplay/test/browser/browser.toml b/dom/media/autoplay/test/browser/browser.toml new file mode 100644 index 0000000000..e68149930c --- /dev/null +++ b/dom/media/autoplay/test/browser/browser.toml @@ -0,0 +1,37 @@ +[DEFAULT] +subsuite = "media-bc" +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_click_to_play.js"] + +["browser_autoplay_policy_detection_global_and_site_sticky.js"] + +["browser_autoplay_policy_detection_global_sticky.js"] + +["browser_autoplay_policy_play_twice.js"] + +["browser_autoplay_policy_request_permission.js"] +https_first_disabled = true + +["browser_autoplay_policy_touchScroll.js"] +https_first_disabled = true + +["browser_autoplay_policy_user_gestures.js"] +https_first_disabled = true + +["browser_autoplay_policy_webRTC_permission.js"] + +["browser_autoplay_policy_web_audio.js"] + +["browser_autoplay_policy_web_audio_with_gum.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..782d131cd3 --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_and_site_sticky.js @@ -0,0 +1,167 @@ +/** + * 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], + ], + }); +}); + +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..51f53969e0 --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_sticky.js @@ -0,0 +1,104 @@ +/** + * 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], + ], + }); +}); + +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..0f7791965c --- /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.startLoadingURIString(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..fa377ea2b1 --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_user_gestures.js @@ -0,0 +1,276 @@ +/* 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], + ], + }); +} + +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.startLoadingURIString(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.startLoadingURIString(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(() => { + Assert.equal(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..14470af9d9 --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio.js @@ -0,0 +1,220 @@ +/** + * 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-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; + Assert.strictEqual(ac.state, "running", `AudioContext is running.`); + } else { + await ac.notAllowedToStart; + Assert.strictEqual( + 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..6e336a5b2d --- /dev/null +++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio_with_gum.js @@ -0,0 +1,173 @@ +/* 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.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; + Assert.strictEqual(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(() => { + Assert.equal(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 @@ +<!DOCTYPE html> +<html> + <head> + <title>Page left intentionally blank...</title> + </head> + <body> + </body> +</html> 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 @@ +<!DOCTYPE html> +<html> +<head> +<title>Non-Autoplay page being used in Iframe</title> +</head> +<body> +<video id="video" src="gizmo.mp4" loop></video> +<script type="text/javascript"> + +const video = document.getElementById("video"); +const w = window.opener || window.parent; + +window.onmessage = async event => { + if (event.data == "play") { + let rv = await video.play().then(() => true, () => false); + w.postMessage(rv ? "played" : "blocked", "*"); + } +} +</script> +</body> +</html> 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 @@ +<!DOCTYPE html> +<head> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> +</head> +<body> +<audio id="testAudio" src="audio.ogg" loop></audio> 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 @@ +<!DOCTYPE html> +<html> +<head> +<title>video</title> +</head> +<body> +<video id="v" src="gizmo.mp4" controls loop></video> +</body> +</html> 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 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>GV autoplay play request test</title> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<script> + +window.addEventListener("message", + (event) => { + // Here we just want to test if media can start from iframe correctly, and + // we don't really care about if it's audible or not. + const isMuted = false; + playAndPostResult(isMuted, event.source); + }); +let w = window.opener || window.parent; +w.postMessage("ready", "*"); + +</script> +</body> +</html> 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 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>GV autoplay play request test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> + </head> +<body> +<script> +/** + * The test info sent from the parent window will determine what kinds of media + * should start, where it should start, the result of the play request and + * whether the document is activated by user gesture. + */ +nextWindowMessage().then( + async (event) => { + let testInfo = event.data; + testInfo.parentWindow = event.source; + await setupTestEnvironment(testInfo); + await startPlaybackAndReturnMessage(testInfo); + }); + +/** + * The following are helper functions. + */ +async function setupTestEnvironment(testInfo) { + if (testInfo.activatedDocument != undefined) { + info(`activate document`); + SpecialPowers.wrap(document).notifyUserGestureActivation(); + } + if (testInfo.iframe != undefined) { + info(`create child frame`); + testInfo.childFrame = await createChildFrame(testInfo); + } +} + +async function createChildFrame(testInfo) { + let frame = document.createElement("iframe"); + let origin = testInfo.iframe == "same-orgin" + ? "http://mochi.test:8888" : "http://example.org"; + frame.src = origin + "/tests/dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_frame.html"; + document.body.appendChild(frame); + info(`waiting for iframe loading`); + is((await nextWindowMessage()).data, "ready", "iframe has finished loading"); + return frame; +} + +async function startPlaybackAndReturnMessage({muted, iframe, parentWindow, childFrame}) { + if (iframe == undefined) { + info(`start playback`); + playAndPostResult(muted, parentWindow); + } else { + info("start autoplay from " + (iframe == "same-origin" ? "same" : "cross") + " origin child frame"); + childFrame.contentWindow.postMessage("play", "*"); + info(`waiting for media calling play from child frame`); + let result = await nextWindowMessage(); + parentWindow.postMessage(result.data, "*"); + } +} + +</script> +</body> +</html> 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 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Autoplay policy frame</title> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <style> + video { + width: 50%; + height: 50%; + } + </style> + </head> + <body> + <script> + window.addEventListener("message", + (event) => { + if (event.data == "click") { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + event.source.postMessage("activated", "*"); + } else if (event.data == "play-audible") { + playAndPostResult(false, event.source); + } else if (event.data == "play-muted") { + playAndPostResult(true, event.source); + } + }); + let w = window.opener || window.parent; + w.postMessage("ready", "*"); + </script> + </body> +</html> 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 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Autoplay policy window</title> + <style> + video { + width: 50%; + height: 50%; + } + </style> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> + </head> + <body> + <pre id="test"> + <script> + + async function createChildFrame(testInfo) { + let frame = document.createElement("iframe"); + let origin = testInfo.same_origin_child + ? "http://mochi.test:8888" : "http://example.org"; + frame.src = origin + "/tests/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_frame.html"; + // Wait for it to load... + document.body.appendChild(frame); + is((await nextWindowMessage()).data, "ready", "Expected a 'ready' message"); + return frame; + } + + async function activateDocument(testInfo) { + // Click the window to activate if appropriate. + if (testInfo.activated_from == "parent") { + info(`activate parent's document`); + SpecialPowers.wrap(document).notifyUserGestureActivation(); + } else if (testInfo.activated_from == "child") { + info(`activate child's document`); + testInfo.childFrame.contentWindow.postMessage("click", "*"); + is((await nextWindowMessage()).data, "activated", "has activated child frame."); + } + } + + function testAutoplayInWindow(testInfo) { + info(`start autoplay from parent frame`); + playAndPostResult(testInfo.muted, testInfo.parentWindow); + } + + async function testAutoplayInChildFrame(testInfo) { + info("start autoplay from " + (testInfo.same_origin_child ? "same" : "cross") + " origin child frame"); + // Ask the child iframe to try to play video. + let play_message = testInfo.muted ? "play-muted" : "play-audible"; + testInfo.childFrame.contentWindow.postMessage(play_message, "*"); + // Wait for the iframe to tell us whether it could play video. + let result = await nextWindowMessage(); + // Report whether the iframe could play to the parent. + testInfo.parentWindow.postMessage(result.data, "*"); + } + + nextWindowMessage().then( + async (event) => { + let testInfo = event.data; + testInfo.parentWindow = event.source; + testInfo.childFrame = await createChildFrame(testInfo); + + await activateDocument(testInfo); + switch (testInfo.play_from) { + case "parent": + testAutoplayInWindow(testInfo); + break; + case "child": + testAutoplayInChildFrame(testInfo); + break; + default: + ok(false, "Incorrect 'play_from' value!") + } + }); + </script> + </pre> + </body> +</html> 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 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Autoplay policy window</title> + <style> + video { + width: 50%; + height: 50%; + } + </style> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> +</head> + +<body> + <pre id="test"> + <script> + + window.ok = window.opener.ok; + window.is = window.opener.is; + window.info = window.opener.info; + + async function testEventDownActivates(eventNames, activator) { + let element = document.createElement("video"); + element.preload = "auto"; + element.src = "short.mp4"; + document.body.appendChild(element); + + await once(element, "loadedmetadata"); + + let played = await element.play().then(() => true, () => false); + ok(!played, "Document should start out not activated, with playback blocked."); + + let x = eventNames.map( + (eventName) => { + return new Promise(function (resolve, reject) { + window.addEventListener(eventName, async function (event) { + let p = await element.play().then(() => true, () => false); + ok(p, "Expect to be activated already in " + eventName); + resolve(); + }); + }); + }); + + activator(); + + await Promise.all(x); + + removeNodeAndSource(element); + } + + nextWindowMessage().then( + async (event) => { + try { + if (event.data == "run keydown test") { + await testEventDownActivates(["keydown", "keypress", "keyup"], () => { + document.body.focus(); + synthesizeKey(" "); + }); + } else if (event.data == "run mousedown test") { + let events = ["mousedown", "mouseup", "click"]; + if (getAndroidVersion() < 0) { + // Non-Android, also listen on pointer events. + events.push("pointerdown", "pointerup"); + } + await testEventDownActivates(events, () => { + synthesizeMouseAtCenter(document.body, {}); + }); + } else { + ok(false, "unexpected message"); + } + } catch (e) { + ok(false, "Caught exception " + e + " " + e.message + " " + e.stackTrace); + } + event.source.postMessage("done", "*"); + }); + + </script> + </pre> +</body> + +</html> 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..b901df3324 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html @@ -0,0 +1,147 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Autoplay policy window</title> + <style> + video { + width: 50%; + height: 50%; + } + :focus { + background-color: blue; + } + </style> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> +</head> + +<body> + <div id="x">This is a div with id=x.</div> + <pre id="test"> + <input type="text" id="text-input"/> + <script> + + window.ok = window.opener.ok; + window.is = window.opener.is; + window.info = window.opener.info; + + // Keys that are expected to be not considered interaction with the page, and + // so not gesture activate the document. + let blacklistKeyPresses = [ + "Tab", + "CapsLock", + "NumLock", + "ScrollLock", + "FnLock", + "Meta", + "Hyper", + "Super", + "ContextMenu", + "ArrowUp", + "ArrowDown", + "ArrowLeft", + "ArrowRight", + "PageUp", + "PageDown", + "Home", + "End", + "Backspace", + "Fn", + "Alt", + "AltGraph", + "Control", + "Shift", + "Escape", + ]; + + let modifiedKeys = [ + { key: "V", modifiers: { altKey: true, shiftKey: true } }, + { key: "a", modifiers: { altKey: true } }, + { key: "a", modifiers: { ctrlKey: true } }, + { key: "KEY_ArrowRight", modifiers: { metaKey: true } }, + { key: "KEY_ArrowRight", modifiers: { altKey: true } }, + ]; + + async function sendInput(element, name, input) { + synthesizeMouseAtCenter(input, {}); + let played = await element.play().then(() => true, () => false); + ok(!played, "Clicking " + name + " should not activate document and should not unblock play"); + + synthesizeCompositionChange({ + composition: { + string: "\u30E9\u30FC\u30E1\u30F3", + clauses: [ + { length: 4, attr: COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + caret: { start: 4, length: 0 } + }); + synthesizeComposition({ type: "compositioncommitasis" }); + played = await element.play().then(() => true, () => false); + ok(!played, "Entering text to " + name + " via IME should not activate document and should not unblock play"); + + input.focus(); + sendString("ascii text"); + played = await element.play().then(() => true, () => false); + ok(!played, "Entering ASCII text into " + name + " should not activate document and should not unblock play"); + + input.blur(); + } + + async function testAutoplayKeyBlacklist(testCase, parent_window) { + let element = document.createElement("video"); + element.preload = "auto"; + element.src = "short.mp4"; + document.body.appendChild(element); + + await once(element, "loadedmetadata"); + + let played = await element.play().then(() => true, () => false); + is(played, false, "Document should start out not activated, with playback blocked."); + + // Try pressing all the keys in the blacklist, then playing. + // Document should not be activated, so play should fail. + + for (let key of blacklistKeyPresses) { + document.body.focus(); + synthesizeKey("KEY_" + key); + played = await element.play().then(() => true, () => false); + is(played, false, "Key " + key + " should not activate document and should not unblock play"); + } + + // Try pressing some keys with modifiers. + let keyNames = (m) => Object.keys(m).join("+"); + for (let x of modifiedKeys) { + document.body.focus(); + synthesizeKey(x.key, x.modifiers); + played = await element.play().then(() => true, () => false); + is(played, false, "Key (" + x.key + "+" + keyNames(x.modifiers) + ") should not activate document and should not unblock play"); + } + + // Try pressing a key not in the blacklist, then playing. + // Document should be activated, and media should play. + synthesizeKey(" "); + played = await element.play().then(() => true, () => false); + is(played, true, "Space key should activate document and should unblock play"); + + removeNodeAndSource(element); + } + + nextWindowMessage().then( + async (event) => { + try { + await testAutoplayKeyBlacklist(event.data, event.source); + } catch (e) { + ok(false, "Caught exception " + e + " " + e.message + " " + e.stackTrace); + } + event.source.postMessage("done", "*"); + }); + + </script> + </pre> +</body> + +</html> 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 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Autoplay policy window</title> + <style> + video { + width: 50%; + height: 50%; + } + </style> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> +</head> + +<body> + <pre id="test"> + <script> + + window.is = window.opener.is; + window.info = window.opener.info; + + async function testPlayBeforeLoadedMetata(testCase, parent_window) { + info("testPlayBeforeLoadedMetata: " + testCase.resource); + + let element = document.createElement("video"); + element.preload = "auto"; + element.muted = testCase.muted; + element.src = testCase.resource; + document.body.appendChild(element); + + is(element.paused, true, testCase.resource + " - should start out paused."); + + let playEventFired = false; + once(element, "play").then(() => { playEventFired = true; }); + let playingEventFired = false; + once(element, "playing").then(() => { playingEventFired = true;}); + let pauseEventFired = false; + once(element, "pause").then(() => { pauseEventFired = true; }); + + let played = await element.play().then(() => true, () => false); + + let playMsg = testCase.resource + " should " + (!testCase.shouldPlay ? "not " : "") + "play"; + is(played, testCase.shouldPlay, playMsg); + is(playEventFired, testCase.shouldPlay, testCase.resource + " - should get play event if we played"); + is(playingEventFired, testCase.shouldPlay, testCase.resource + "- should get playing event if we played"); + is(pauseEventFired, false, testCase.resource + " - should not get pause event if we played"); + removeNodeAndSource(element); + } + + nextWindowMessage().then( + async (event) => { + await testPlayBeforeLoadedMetata(event.data, event.source); + event.source.postMessage("done", "*"); + }); + + </script> + </pre> +</body> + +</html> 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 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Autoplay policy window</title> + <style> + video { + width: 50%; + height: 50%; + } + </style> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> +</head> + +<body> + <pre id="test"> + <script> + + window.is = window.opener.is; + window.info = window.opener.info; + + function testAutoplayUnmutePauses(testCase, parent_window) { + return new Promise(function (resolve, reject) { + + info("testAutoplayUnmutePauses: " + testCase.property); + + let element = document.createElement("video"); + element.preload = "auto"; + + // Make inaudible. + element[testCase.property] = testCase.inaudible; + + // Once we've loaded, play, then make audible. + // Assert that the media is paused when we make it audible. + element.addEventListener("loadeddata", () => { + info("loadeddata"); + element.play(); + is(element.paused, false, testCase.property + "=" + testCase.inaudible + " - should be playing"); + element[testCase.property] = testCase.audible; + is(element.paused, true, testCase.property + "=" + testCase.audible + " - should be paused."); + resolve(); + }); + + element.src = "short.mp4"; + element.id = "video"; + document.body.appendChild(element); + }); + } + + nextWindowMessage().then( + (event) => { + testAutoplayUnmutePauses(event.data, event.source) + .then(() => { + event.source.postMessage("done", "*"); + }); + }); + + </script> + </pre> +</body> + +</html> diff --git a/dom/media/autoplay/test/mochitest/mochitest.toml b/dom/media/autoplay/test/mochitest/mochitest.toml new file mode 100644 index 0000000000..0b6f0a169f --- /dev/null +++ b/dom/media/autoplay/test/mochitest/mochitest.toml @@ -0,0 +1,69 @@ +[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 = ["os != 'android'"] + +["test_autoplay_policy.html"] + +["test_autoplay_policy_activation.html"] + +["test_autoplay_policy_eventdown_activation.html"] + +["test_autoplay_policy_key_blacklist.html"] +skip-if = ["verify && debug && os == 'win'"] # bug 1424903 + +["test_autoplay_policy_permission.html"] + +["test_autoplay_policy_play_before_loadedmetadata.html"] +skip-if = ["os == 'android'"] # bug 1591121 + +["test_autoplay_policy_unmute_pauses.html"] + +["test_autoplay_policy_web_audio_AudioParamStream.html"] + +["test_autoplay_policy_web_audio_createMediaStreamSource.html"] + +["test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html"] + +["test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.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 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Media test: autoplay attribute</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> +</head> +<body> +<video id='v1'"></video><audio id='a1'></audio> +<video id='v2' autoplay></video><audio id='a2' autoplay></audio> +<pre id="test"> +<script class="testbody" type="text/javascript"> +/* import-globals-from ../../../test/manifest.js */ +var v1 = document.getElementById('v1'); +var a1 = document.getElementById('a1'); +var v2 = document.getElementById('v2'); +var a2 = document.getElementById('a2'); +ok(!v1.autoplay, "v1.autoplay should be false by default"); +ok(!a1.autoplay, "v1.autoplay should be false by default"); +ok(v2.autoplay, "v2.autoplay should be true"); +ok(a2.autoplay, "v2.autoplay should be true"); + +v1.autoplay = true; +a1.autoplay = true; +ok(v1.autoplay, "video.autoplay not true"); +ok(a1.autoplay, "audio.autoplay not true"); +is(v1.getAttribute("autoplay"), "", "video autoplay attribute not set"); +is(a1.getAttribute("autoplay"), "", "video autoplay attribute not set"); + +mediaTestCleanup(); + +</script> +</pre> +</body> +</html> 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 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Media test: play() method</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> +</head> +<body contenteditable="true"> +<pre id="test"> + +<script> +/* import-globals-from ../../../test/manifest.js */ +var manager = new MediaTestManager; + +var tokens = { + 0: ["canplay"], + "canplay": ["canplay", "canplaythrough"], + "canplaythrough": ["canplay", "canplaythrough"] +}; + +var eventList = ["play", "canplay", "playing", "canplaythrough", "ended"]; + +function gotPlayEvent(event) { + var v = event.target; + ok(tokens[v._state].includes(event.type), + "Check expected event got " + event.type + + " at " + v._state + " for " + v._name); + v._state = event.type; + if (event.type == 'canplaythrough') { + // Remove all event listeners to avoid running tests after finishing test case. + eventList.forEach(function (e) { + v.removeEventListener(e, gotPlayEvent); + }); + v.pause(); + goToNext(v); + } +} + +function goToNext(v) { + v.remove(); + manager.finished(v.token); +} + +function initTest(test, token) { + var v = document.createElement('video'); + v.preload = "metadata"; + v.token = token; + manager.started(token); + v._state = 0; + + eventList.forEach(function (e) { + v.addEventListener(e, gotPlayEvent); + }); + + v.src = test.name; + v._name = test.name; + v.autoplay = true; + document.body.appendChild(v); // Causes load. +} + +manager.runTests(gSmallTests, initTest); + +</script> +</pre> +</body> +</html> 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 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>GV Autoplay policy test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> + </head> +<body> +<script> + +/** + * On GeckoView, we have a different autoplay policy check than the one on other + * platforms, which would send a request to the embedding app to ask if the + * media can be allowed to play. We use a testing pref to simulate the response + * from the request. + * + * The request has two types, audible and inaudible request. The result of the + * audible request would only take effect on audible media, and the result of + * inaudible request would only take effect on inaudible media. + * + * User activation policy still work on GeckoView, so once the page has been + * activated, then we won't have to send the request and would allow all media + * in that page to play. + * + * The following test cases contain the information which would be applied in + * test, and the expected result of the test. For example, the following info + * indicates that, play an [inaudible] media in the environment with [allowed] + * [audible] request, and we expect to see it plays successfully. + * - muted: false, + * - requestType: "audible", + * - requestResult: "allowed", + * - expectedPlayResult: true, + */ +const testCases = [ + // (1) testing audible playback + { + name: "[audible] playback and [allowed audible request] -> allowed", + muted: false, + requestType: "audible", + requestResult: "allowed", + expectedPlayResult: true, + }, + { + name: "[audible] playback and [denied audible request] -> blocked", + muted: false, + requestType: "audible", + requestResult: "denied", + expectedPlayResult: false, + }, + { + name: "[audible] playback and [allowed inaudible request] -> blocked", + muted: false, + requestType: "inaudible", + requestResult: "allowed", + expectedPlayResult: false, + }, + { + name: "[audible] playback and [denied inaudible request] -> blocked", + muted: false, + requestType: "inaudible", + requestResult: "denied", + expectedPlayResult: false, + }, + { + name: "[audible] playback with [pending request] in [activated document] -> allowed", + muted: false, + requestType: "all", + requestResult: "pending", + activatedDocument: true, + expectedPlayResult: true, + }, + { + name: "[audible] playback with [denied audible request] in [activated document] -> allowed", + muted: false, + requestType: "audible", + requestResult: "allowed", + activatedDocument: true, + expectedPlayResult: true, + }, + { + name: "[audible] playback with [pending request] in [unactivated document] -> blocked", + muted: false, + requestType: "all", + requestResult: "pending", + expectedPlayResult: false, + }, + // (2) testing inaudible playback + { + name: "[inaudible] playback and [allowed audible request] -> blocked", + muted: true, + requestType: "audible", + requestResult: "allowed", + expectedPlayResult: false, + }, + { + name: "[inaudible] playback and [denied audible request] -> blocked", + muted: true, + requestType: "audible", + requestResult: "denied", + expectedPlayResult: false, + }, + { + name: "[inaudible] playback and [allowed inaudible request] -> allowed", + muted: true, + requestType: "inaudible", + requestResult: "allowed", + expectedPlayResult: true, + }, + { + name: "[inaudible] playback and [denied inaudible request] -> blocked", + muted: true, + requestType: "inaudible", + requestResult: "denied", + expectedPlayResult: false, + }, + { + name: "[inaudible] playback without [pending request] in [activated document] -> allowed", + muted: true, + requestType: "all", + requestResult: "pending", + activatedDocument: true, + expectedPlayResult: true, + }, + { + name: "[inaudible] playback without [denied inaudible request] in [activated document] -> allowed", + muted: true, + requestType: "inaudible", + requestResult: "denied", + activatedDocument: true, + expectedPlayResult: true, + }, + { + name: "[inaudible] playback without [pending request] in [unactivated document] -> blocked", + muted: true, + requestType: "all", + requestResult: "pending", + expectedPlayResult: false, + }, + // (3) testing playback from iframe + { + name: "playback from [same origin] iframe and [allowed all request]-> allowed", + requestType: "all", + requestResult: "allowed", + iframe: "same-origin", + expectedPlayResult: true, + }, + { + name: "playback from [same origin] iframe and [denied all request]-> blocked", + requestType: "all", + requestResult: "denied", + iframe: "same-origin", + expectedPlayResult: false, + }, + { + name: "playback from [cross origin] iframe and [allowed all request]-> allowed", + requestType: "all", + requestResult: "allowed", + iframe: "cross-origin", + expectedPlayResult: true, + }, + { + name: "playback from [cross origin] iframe and [denied all request]-> blocked", + requestType: "all", + requestResult: "denied", + iframe: "cross-origin", + expectedPlayResult: false, + }, +]; + +const pageURL = "file_autoplay_gv_play_request_window.html"; + +SimpleTest.waitForExplicitFinish(); + +(async function startTest() { + for (const testCase of testCases) { + info(`- start running test '${testCase.name}'-`); + await setTestingPrefs(testCase); + + // Run each test in a new window to ensure they won't interfere each other + const testPage = window.open(pageURL, "", "width=500,height=500"); + await once(testPage, "load"); + testPage.postMessage(testCase, window.origin); + let result = await nextWindowMessage(); + is(result.data.allowedToPlay, testCase.expectedPlayResult, `allowed - ${testCase.name}`); + is(result.data.played, testCase.expectedPlayResult, `played - ${testCase.name}`); + testPage.close(); + } + SimpleTest.finish(); +})(); + +/** + * This function would set which type of request would be explicitly allowed, + * and the type of request we don't mention about would be pending forever. + * E.g. `setTestingPrefs({"audible", "allow"})` will allow the audible request + * and leave the inaudible request pending forever. + */ +async function setTestingPrefs({requestType, requestResult}) { + let prefVal = 0; + if (requestType == "all") { + if (requestResult == "pending") { + prefVal = 7; + } else { + prefVal = requestResult == "allowed" ? 1 : 2; + } + } else if (requestType == "audible") { + prefVal = requestResult == "allowed" ? 3 : 4; + } else if (requestType == "inaudible") { + prefVal = requestResult == "allowed" ? 5 : 6; + } + info(`set testing pref to ${prefVal}`); + await SpecialPowers.pushPrefEnv({ + set: [["media.geckoview.autoplay.request.testing", prefVal], + ["media.geckoview.autoplay.request", true]], + }); +} + +</script> +</body> +</html> 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 @@ + +<!DOCTYPE HTML> +<html> +<head> + <title>Autoplay policy test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> +</head> +<body> +<pre id="test"> + +<script> +/* import-globals-from ../../../test/manifest.js */ +let manager = new MediaTestManager; + +gTestPrefs.push(["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0]); + +window.info = function(msg, token) { + SimpleTest.info(msg + ", token=" + token); +} + +window.is = function(valA, valB, msg, token) { + SimpleTest.is(valA, valB, msg + ", token=" + token); +} + +window.ok = function(val, msg, token) { + SimpleTest.ok(val, msg + ", token=" + token); +} + +/** + * test files and paremeters + */ +var autoplayTests = [ + /* video */ + { name: "gizmo.mp4", type: "video/mp4", hasAudio:true }, + { name: "gizmo-noaudio.mp4", type: "video/mp4", hasAudio:false }, + { name: "gizmo.webm", type: "video/webm", hasAudio:true }, + { name: "gizmo-noaudio.webm", type: "video/webm", hasAudio:false }, + /* audio */ + { name: "small-shot.ogg", type: "audio/ogg", hasAudio:true }, + { name: "small-shot.m4a", type: "audio/mp4", hasAudio:true }, + { name: "small-shot.mp3", type: "audio/mpeg", hasAudio:true }, + { name: "small-shot.flac", type: "audio/flac", hasAudio:true }, +]; + +var autoplayParams = [ + { volume: 1.0, muted: false, preload: "none" }, + { volume: 0.0, muted: false, preload: "none" }, + { volume: 1.0, muted: true, preload: "none" }, + { volume: 0.0, muted: true, preload: "none" }, + { volume: 1.0, muted: false, preload: "metadata" }, + { volume: 0.0, muted: false, preload: "metadata" }, + { volume: 1.0, muted: true, preload: "metadata" }, + { volume: 0.0, muted: true, preload: "metadata" }, +]; + +function createTestArray() +{ + var tests = []; + for (let test of autoplayTests) { + for (let param of autoplayParams) { + tests.push({ + name: test.name, + type: test.type, + hasAudio: test.hasAudio, + volume: param.volume, + muted: param.muted, + preload: param.preload, + }); + } + } + return tests; +} + +/** + * Main test function for different autoplay cases without user interaction. + * + * When the pref "media.autoplay.default" is 1 and the pref + * "media.autoplay.blocking_policy" is 0, we only allow + * audible media to autoplay after the website has been activated by specific + * user gestures. However, inaudible media won't be restricted. + * + * Audible means the volume is not zero, or muted is not true for the video with + * audio track. For media without loading metadata, we can't know whether it has + * audio track or not, so we would also regard it as audible media. + * + * Inaudible means the volume is zero, or the muted is true, or the video without + * audio track. + */ +async function runTest(test, token) { + manager.started(token); + + await testPlay(test, token); + await testAutoplayKeyword(test, token); + + manager.finished(token); +} + +manager.runTests(createTestArray(), runTest); + +/** + * Different test scenarios + */ +async function testPlay(test, token) { + info("### start testPlay", token); + info(`volume=${test.volume}, muted=${test.muted}, ` + + `preload=${test.preload}, hasAudio=${test.hasAudio}`, token); + + let element = document.createElement(getMajorMimeType(test.type)); + element.volume = test.volume; + element.muted = test.muted; + element.src = test.name; + document.body.appendChild(element); + + // Only need to test preload when calling play(), because media with 'autoplay' + // keyword always starts after loading enough data. + const preLoadNone = test.preload == "none"; + if (!preLoadNone) { + info("### wait for loading metadata", token); + await once(element, "loadedmetadata"); + } + + let isAudible = (preLoadNone || test.hasAudio) && + test.volume != 0.0 && + !test.muted; + let state = isAudible? "audible" : "non-audible"; + info(`### calling play() for ${state} media`, token); + let promise = element.play(); + if (isAudible) { + await promise.catch(function(error) { + ok(element.paused, `${state} media fail to start via play()`, token); + is(error.name, "NotAllowedError", "rejected play promise", token); + }); + } else { + // since we just want to check the value of 'paused', we don't need to wait + // resolved play promise. (it's equal to wait for 'playing' event) + await once(element, "play"); + ok(!element.paused, `${state} media start via play()`, token); + } + + removeNodeAndSource(element); +} + +async function testAutoplayKeyword(test, token) { + info("### start testAutoplayKeyword", token); + info(`volume=${test.volume}, muted=${test.muted}, ` + + `hasAudio=${test.hasAudio}`, token); + + let element = document.createElement(getMajorMimeType(test.type)); + element.autoplay = true; + element.volume = test.volume; + element.muted = test.muted; + element.src = test.name; + document.body.appendChild(element); + + let isAudible = test.hasAudio && + test.volume != 0.0 && + !test.muted; + let state = isAudible? "audible" : "non-audible"; + info(`### wait to autoplay for ${state} media`, token); + if (isAudible) { + await once(element, "canplay"); + ok(element.paused, `can not start with 'autoplay' keyword for ${state} media`, token); + } else { + await once(element, "play"); + ok(!element.paused, `start with 'autoplay' keyword for ${state} media`, token); + } + + removeNodeAndSource(element); +} + +</script> 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 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Autoplay policy test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> + </head> + <body> + <pre id="test"> + <script> + + // Tests that videos can only play audibly in windows/frames + // which have been activated by same-origin user gesture. + + gTestPrefs.push(["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0]); + + SpecialPowers.pushPrefEnv({'set': gTestPrefs}, () => { + runTest(); + }); + + let test_cases = [ + { + name: "inaudible playback in unactivated same-origin iframe in activated parent -> allowed", + muted: true, + same_origin_child: true, + activated_from: "parent", + play_from: "child", + should_play: true, + }, + + { + name: "inaudible playback in unactivated same-origin iframe in unactivated parent -> allowed", + muted: true, + same_origin_child: true, + activated_from: "none", + play_from: "child", + should_play: true, + }, + + { + name: "audible playback in unactivated same-origin iframe in activated parent -> allowed", + muted: false, + same_origin_child: true, + activated_from: "parent", + play_from: "child", + should_play: true, + }, + + { + name: "audible playback in unactivated same-origin iframe in unactivated parent -> blocked", + muted: false, + same_origin_child: true, + activated_from: "none", + play_from: "child", + should_play: false, + }, + + { + name: "inaudible playback in unactivated cross-origin iframe in activated parent -> allowed", + muted: true, + same_origin_child: false, + activated_from: "parent", + play_from: "child", + should_play: true, + }, + + { + name: "inaudible playback in unactivated cross-origin iframe in unactivated parent -> allowed", + muted: true, + same_origin_child: false, + activated_from: "none", + play_from: "child", + should_play: true, + }, + + { + name: "audible playback in unactivated cross-origin iframe in activated parent -> allowed", + muted: false, + same_origin_child: false, + activated_from: "parent", + play_from: "child", + should_play: true, + }, + + { + name: "audible playback in unactivated cross-origin iframe in unactivated parent -> blocked", + muted: false, + same_origin_child: false, + activated_from: "none", + play_from: "child", + should_play: false, + }, + + { + name: "audible playback in activated cross-origin iframe -> allowed", + muted: false, + same_origin_child: false, + activated_from: "child", + play_from: "child", + should_play: true, + }, + + { + name: "audible playback in activated document -> allowed", + muted: false, + activated_from: "parent", + play_from: "parent", + should_play: true, + }, + + { + name: "audible playback in unactivated document -> blocked", + muted: false, + activated_from: "none", + play_from: "parent", + should_play: false, + }, + + { + name: "audible playback in activated document (via cross-origin child) -> allowed", + muted: false, + same_origin_child: false, + activated_from: "child", + play_from: "parent", + should_play: true, + }, + + { + name: "audible playback in activated document (via same-origin child) -> allowed", + muted: false, + same_origin_child: true, + activated_from: "child", + play_from: "parent", + should_play: true, + }, + + { + name: "inaudible playback in activated document -> allowed", + muted: true, + activated_from: "parent", + play_from: "parent", + should_play: true, + }, + + { + name: "inaudible playback in unactivated document -> allowed", + muted: true, + activated_from: "none", + play_from: "parent", + should_play: true, + }, + + ]; + + let child_url = "file_autoplay_policy_activation_window.html"; + + async function runTest() { + for (const test_case of test_cases) { + // Run each test in a new window, to ensure its user gesture + // activation state isn't tainted by preceeding tests. + let child = window.open(child_url, "", "width=500,height=500"); + await once(child, "load"); + child.postMessage(test_case, window.origin); + let result = await nextWindowMessage(); + SimpleTest.is(result.data.allowedToPlay, test_case.should_play, "allowed - " + test_case.name); + SimpleTest.is(result.data.played, test_case.should_play, "played - " + test_case.name); + child.close(); + } + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + + </script> + </pre> + </body> +</html> 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 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Autoplay policy test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> +</head> + +<body> + <pre id="test"> + <script> + + // Tests that we gesture activate on mousedown and keydown. + + gTestPrefs.push(["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0]); + + SpecialPowers.pushPrefEnv({ 'set': gTestPrefs }, () => { + runTest(); + }); + + let child_url = "file_autoplay_policy_eventdown_activation.html"; + + async function runTest() { + // Run test in a new window, to ensure its user gesture + // activation state isn't tainted by preceeding tests. + { + let child = window.open(child_url, "", "width=500,height=500"); + await once(child, "load"); + child.postMessage("run keydown test", window.origin); + await nextWindowMessage(); + child.close(); + } + + { + let child = window.open(child_url, "", "width=500,height=500"); + await once(child, "load"); + child.postMessage("run mousedown test", window.origin); + await nextWindowMessage(); + child.close(); + } + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + + </script> + </pre> +</body> + +</html> 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 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Autoplay policy test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> +</head> + +<body> + <pre id="test"> + <script> + + // Tests that keypresses for non-printable characters, + // and mouse/keyboard interaction with editable elements, + // don't gesture activate documents, and don't unblock + // audible autoplay. + + gTestPrefs.push(["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0]); + + SpecialPowers.pushPrefEnv({ 'set': gTestPrefs }, () => { + runTest(); + }); + + let child_url = "file_autoplay_policy_key_blacklist.html"; + + async function runTest() { + // Run test in a new window, to ensure its user gesture + // activation state isn't tainted by preceeding tests. + let child = window.open(child_url, "", "width=500,height=500"); + await once(child, "load"); + child.postMessage("run test", window.origin); + await nextWindowMessage(); + child.close(); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + + </script> + </pre> +</body> + +</html> 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 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Autoplay policy test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> +</head> + +<body> + <pre id="test"> + <script> + + // Tests that origins with "autoplay-media" permission can autoplay. + + gTestPrefs.push(["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0]); + + SpecialPowers.pushPrefEnv({ 'set': gTestPrefs }, () => { + runTest(); + }); + + async function testPlayInOrigin(testCase) { + // Run test in a new window, to ensure its user gesture + // activation state isn't tainted by preceeding tests. + let url = testCase.origin + "/tests/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_frame.html"; + let child = window.open(url, "", "width=500,height=500"); + is((await nextWindowMessage()).data, "ready", "Expected a 'ready' message"); + child.postMessage("play-audible", testCase.origin); + // Wait for the window to tell us whether it could play video. + let result = await nextWindowMessage(); + is(result.data.allowedToPlay, testCase.shouldPlay, "allowedToPlay - " + testCase.message); + is(result.data.played, testCase.shouldPlay, "played - " + testCase.message); + child.close(); + } + + async function runTest() { + // First verify that we can't play in a document unwhitelisted. + is(window.origin, "http://mochi.test:8888", "Origin should be as we assume, otherwise the rest of the test is bogus!"); + + await testPlayInOrigin({ + origin: "http://mochi.test:8888", + shouldPlay: false, + message: "Should not be able to play unwhitelisted." + }); + + // Add our origin to the whitelist. + await pushAutoplayAllowedPermission(); + + // Now we should be able to play... + await testPlayInOrigin({ + origin: "http://mochi.test:8888", + shouldPlay: true, + message: "Should be able to play since whitelisted." + }); + + // But sub-origins should not. + await testPlayInOrigin({ + origin: "http://test1.mochi.test:8888", + shouldPlay: false, + message: "Sub origin should not count as whitelisted." + }); + await testPlayInOrigin({ + origin: "http://sub1.test1.mochi.test:8888", + shouldPlay: false, + message: "Sub-sub-origins should not count as whitelisted." + }); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + + </script> + </pre> +</body> + +</html> 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 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Autoplay policy test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> +</head> + +<body> + <pre id="test"> + <script> + + window.is = SimpleTest.is; + window.info = SimpleTest.info; + + // Tests that videos which have no audio track will play if play() + // is called before the video has reached readyState >= HAVE_METADATA. + + gTestPrefs.push(["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0]); + + SpecialPowers.pushPrefEnv({ 'set': gTestPrefs }, () => { + runTest(); + }); + + let testCases = [ + { + resource: "320x240.ogv", // Only video track. + shouldPlay: false, + muted: false, + }, + { + resource: "320x240.ogv", // Only video track. + shouldPlay: true, + muted: true, + }, + { + resource: "short.mp4", // Audio and video track. + shouldPlay: false, + muted: false, + }, + { + resource: "short.mp4", // Audio and video track. + shouldPlay: true, + muted: true, + }, + ]; + + let child_url = "file_autoplay_policy_play_before_loadedmetadata.html"; + + async function runTest() { + for (const testCase of testCases) { + // Run each test in a new window, to ensure its user gesture + // activation state isn't tainted by preceeding tests. + let child = window.open(child_url, "", "width=500,height=500"); + await once(child, "load"); + child.postMessage(testCase, window.origin); + await nextWindowMessage(); + child.close(); + } + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + + </script> + </pre> +</body> + +</html> 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 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Autoplay policy test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> + <script type="text/javascript" src="AutoplayTestUtils.js"></script> +</head> + +<body> + <pre id="test"> + <script> + + window.is = SimpleTest.is; + window.info = SimpleTest.info; + + // Tests that videos can only play audibly in windows/frames + // which have been activated by same-origin user gesture. + + gTestPrefs.push(["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0]); + + SpecialPowers.pushPrefEnv({ 'set': gTestPrefs }, () => { + runTest(); + }); + + let testCases = [ + { + property: "muted", + inaudible: true, + audible: false, + }, + + { + property: "volume", + inaudible: 0.0, + audible: 1.0, + }, + ]; + + let child_url = "file_autoplay_policy_unmute_pauses.html"; + + async function runTest() { + for (const testCase of testCases) { + // Run each test in a new window, to ensure its user gesture + // activation state isn't tainted by preceeding tests. + let child = window.open(child_url, "", "width=500,height=500"); + await once(child, "load"); + child.postMessage(testCase, window.origin); + await nextWindowMessage(); + child.close(); + } + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + + </script> + </pre> +</body> + +</html> 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..4a891c2815 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_AudioParamStream.html @@ -0,0 +1,170 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Autoplay policy test : suspend/resume the AudioParam's stream</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> +</head> +<body> +<script> + +/** + * This test is used to ensure the AudioParam's stream can be suspended/resumed + * by AudioContext. + */ + +SimpleTest.waitForExplicitFinish(); + +(async function testSuspendAndResumeAudioParamStreams() { + await setupTestPreferences(); + + info(`- create the AudioContext -`); + createAudioContext(); + + info(`- the AudioContext is not allowed to start in beginning -`); + await audioContextShouldBeBlocked(); + + info(`- connect AudioScheduledSourceNode to the AudioParam and start AudioScheduledSourceNode, the AudioParam's stream should be suspended in the beginning -`) + let audioParamsArr = await connectAudioScheduledSourceNodeToAudioParams(); + + info(`- the AudioContext and the AudioParam's stream should be resumed -`); + await audioContextAndAudioParamStreamsShouldBeResumed(audioParamsArr); + + info(`- suspend the AudioContext which should also suspend the AudioParam's stream -`); + await suspendAudioContextAndAudioParamStreams(audioParamsArr); + + endTest(); +})(); + +/** + * Test utility 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], + ]}); +} + +function createAudioContext() { + /* global ac */ + window.ac = new AudioContext(); + + ac.allowedToStart = new Promise(resolve => { + ac.addEventListener("statechange", function() { + if (ac.state === "running") { + resolve(); + } + }, {once: true}); + }); + + ac.notAllowedToStart = new Promise(resolve => { + ac.addEventListener("blocked", async function() { + resolve(); + }, {once: true}); + }); +} + +async function audioContextShouldBeBlocked() { + await ac.notAllowedToStart; + is(ac.state, "suspended", `AudioContext is blocked.`); +} + +function createAudioParams(nodeType) { + switch (nodeType) { + case "audioBufferSource": + let buffer = ac.createBufferSource(); + return [buffer.playbackRate, buffer.detune]; + case "biquadFilter": + let bf = ac.createBiquadFilter(); + return [bf.frequency, bf.detune, bf.Q, bf.gain]; + case "constantSource": + return [ac.createConstantSource().offset]; + case "dynamicsCompressor": + let dc = ac.createDynamicsCompressor(); + return [dc.threshold, dc.knee, dc.ratio, dc.attack, dc.release]; + case "delay": + return [ac.createDelay(5.0).delayTime]; + case "gain": + return [ac.createGain().gain]; + case "oscillator": + let osc = ac.createOscillator(); + return [osc.frequency, osc.detune]; + case "panner": + let panner = ac.createPanner(); + return [panner.positionX, panner.positionY, panner.positionZ, + panner.orientationX, panner.orientationY, panner.orientationZ]; + case "stereoPanner": + return [ac.createStereoPanner().pan]; + default: + ok(false, `non-defined node type ${nodeType}.`); + return []; + } +} + +function createAudioParamArrWithName(nodeType) { + let audioParamsArr = createAudioParams(nodeType); + for (let audioParam of audioParamsArr) { + audioParam.name = nodeType; + } + return audioParamsArr; +} + +function createAllAudioParamsFromDifferentAudioNode() { + const NodesWithAudioParam = + ["audioBufferSource", "biquadFilter", "constantSource", "delay", + "dynamicsCompressor", "gain", "oscillator", "panner", "stereoPanner"]; + let audioParamsArr = []; + for (let nodeType of NodesWithAudioParam) { + audioParamsArr = audioParamsArr.concat(createAudioParamArrWithName(nodeType)); + } + ok(audioParamsArr.length >= NodesWithAudioParam.length, + `Length of AudioParam array (${audioParamsArr.length}) is longer than the " + "length of node type array (${NodesWithAudioParam.length}).`); + return audioParamsArr; +} + +function connectAudioScheduledSourceNodeToAudioParams() { + let osc = ac.createOscillator(); + let audioParamsArr = createAllAudioParamsFromDifferentAudioNode(); + for (let audioParam of audioParamsArr) { + osc.connect(audioParam); + ok(SpecialPowers.wrap(audioParam).isTrackSuspended, + `(${audioParam.name}) audioParam's stream has been suspended.`); + } + + // simulate user gesture in order to start video. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + osc.start(); + return audioParamsArr; +} + +async function audioContextAndAudioParamStreamsShouldBeResumed(audioParamsArr) { + await ac.allowedToStart; + is(ac.state, "running", `AudioContext is allowed to start.`); + for (let audioParam of audioParamsArr) { + ok(!SpecialPowers.wrap(audioParam).isTrackSuspended, + `(${audioParam.name}) audioParam's stream has been resumed.`);; + } +} + +async function suspendAudioContextAndAudioParamStreams(audioParamsArr) { + await ac.suspend(); + is(ac.state, "suspended", `AudioContext is suspended.`); + for (let audioParam of audioParamsArr) { + ok(SpecialPowers.wrap(audioParam).isTrackSuspended, + `(${audioParam.name}) audioParam's stream has been suspended.`);; + } +} + +function endTest() { + // reset the activation flag in order not to interfere following test in the + // verify mode which would run the test using same document couple times. + SpecialPowers.wrap(document).clearUserGestureActivation(); + SimpleTest.finish(); +} + +</script> 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..e718e7e593 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_createMediaStreamSource.html @@ -0,0 +1,118 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Autoplay policy test : createMediaStreamSource with active stream</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> +</head> +<body> +<script> + +/** + * This test is used to ensure that we would try to start the blocked AudioContext + * which is blocked by autoplay policy, when it creates a MediaStreamAudioSourceNode + * which has a active input stream. + */ + +SimpleTest.waitForExplicitFinish(); + +(async function testStartAudioContextWhenCreatingMediaStreamAudioSourceWithActiveStream() { + await setupTestPreferences(); + + info(`- create 2 AudioContext, one is used to generate active stream, another one is used to test whether it would be resumed after starting MediaStreamAudioSource with active stream -`); + createAudioContexts(); + + info(`- both AudioContext are not allowed to start in beginning -`); + await audioContextsShouldBeBlocked(); + + info(`- using AudioContext2 to create a MediaStreamAudioSourceNode with active stream, which should resume AudioContext2 -`); + await createMediaStreamAudioSourceByAudioContext2(); + + endTest(); +})(); + +/** + * Test utility 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], + ]}); +} + +function createAudioContexts() { + /* global ac1, ac2 */ + window.ac1 = new AudioContext(); + window.ac2 = new AudioContext(); + + ac1.allowedToStart = new Promise(resolve => { + ac1.addEventListener("statechange", function() { + if (ac1.state === "running") { + resolve(); + } + }, {once: true}); + }); + + ac1.notAllowedToStart = new Promise(resolve => { + ac1.addEventListener("blocked", async function() { + resolve(); + }, {once: true}); + }); + + + ac2.allowedToStart = new Promise(resolve => { + ac2.addEventListener("statechange", function() { + if (ac2.state === "running") { + resolve(); + } + }, {once: true}); + }); + + ac2.notAllowedToStart = new Promise(resolve => { + ac2.addEventListener("blocked", async function() { + resolve(); + }, {once: true}); + }); +} + +async function audioContextsShouldBeBlocked() { + await ac1.notAllowedToStart; + await ac2.notAllowedToStart; + is(ac1.state, "suspended", `AudioContext1 is blocked.`); + is(ac2.state, "suspended", `AudioContext2 is blocked.`); +} + +async function startAudioContext1() { + // simulate user gesture in order to start video. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + ok(await ac1.resume().then(() => true, () => false), `resumed AudioContext1.`); + await ac1.allowedToStart; + is(ac1.state, "running", `AudioContext1 is running.`); +} + +async function getActiveStream() { + await startAudioContext1(); + // As AudioContext1 has been resumed, we can use it to create active stream. + return ac1.createMediaStreamDestination().stream; +} + +async function createMediaStreamAudioSourceByAudioContext2() { + is(ac2.state, "suspended", `AudioContext2 is suspended.`); + let source = ac2.createMediaStreamSource(await getActiveStream()); + source.connect(ac2.destination); + await ac2.allowedToStart; + is(ac2.state, "running", `AudioContext2 is running.`); +} + +function endTest() { + // reset the activation flag in order not to interfere following test in the + // verify mode which would run the test using same document couple times. + SpecialPowers.wrap(document).clearUserGestureActivation(); + SimpleTest.finish(); +} + +</script> 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..27eb3ca45d --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html @@ -0,0 +1,104 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Autoplay policy test : use media element as source for web audio</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> +</head> +<body> +<script> +/* import-globals-from ../../../test/manifest.js */ +/** + * This test is used to ensure blocked AudioContext would be resumed when the + * source media element of MediaElementAudioSouceNode which has been created and + * connected to destinationnode starts. + */ + +SimpleTest.waitForExplicitFinish(); + +(async function testResumeAudioContextWhenMediaElementSourceStarted() { + await setupTestPreferences(); + + info(`- create audio context -`); + createAudioContext(); + + info(`- AudioContext is not allowed to start in beginning -`); + await audioContextShouldBeBlocked(); + + info(`- create a source for web audio and start the source -`); + await useMediaElementAsSourceAndPlayMediaElement(); + + info(`- AudioContext should be allowed to start after MediaElementAudioSourceNode started -`); + await audioContextShouldBeAllowedToStart(); + + endTest(); +})(); + +/** + * Test utility 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], + ]}); +} + +function createAudioContext() { + /* global ac */ + window.ac = new AudioContext(); + + ac.allowedToStart = new Promise(resolve => { + ac.addEventListener("statechange", function() { + if (ac.state === "running") { + resolve(); + } + }, {once: true}); + }); + + ac.notAllowedToStart = new Promise(resolve => { + ac.addEventListener("blocked", async function() { + resolve(); + }, {once: true}); + }); +} + +async function audioContextShouldBeBlocked() { + await ac.notAllowedToStart; + is(ac.state, "suspended", `AudioContext is blocked.`); +} + +async function useMediaElementAsSourceAndPlayMediaElement() { + let video = document.createElement('video'); + video.src = "gizmo.mp4"; + + let source = ac.createMediaElementSource(video); + source.connect(ac.destination); + // simulate user gesture in order to start video. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + await playVideo(video); +} + +async function playVideo(video) { + video.play(); + await once(video, "play"); + ok(true, `video started.`); + removeNodeAndSource(video); +} + +async function audioContextShouldBeAllowedToStart() { + await ac.allowedToStart; + is(ac.state, "running", `AudioContext is allowed to start.`); +} + +function endTest() { + // reset the activation flag in order not to interfere following test in the + // verify mode which would run the test using same document couple times. + SpecialPowers.wrap(document).clearUserGestureActivation(); + SimpleTest.finish(); +} + +</script> 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..1fd1162c07 --- /dev/null +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Autoplay policy test : do not resume AudioContext which is suspended by page</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> +</head> +<body> +<script> +/* import-globals-from ../../../test/manifest.js */ +/** + * This test is used to ensure we won't resume AudioContext which is suspended + * by page (it means calling suspend() explicitly) when calling + * `AudioScheduledSourceNode.start()`. + */ + +SimpleTest.waitForExplicitFinish(); + +(async function testNotResumeUserInvokedSuspendedAudioContext() { + await setupTestPreferences(); + + const nodeTypes = ["AudioBufferSourceNode", "ConstantSourceNode", "OscillatorNode"]; + for (let nodeType of nodeTypes) { + info(`- create an audio context which should not be allowed to start, it's allowed to be created, but it's forbidden to start -`); + await createAudioContext(); + + info(`- explicitly suspend the AudioContext in the page -`); + suspendAudioContext(); + + info(`- start an 'AudioScheduledSourceNode', and check that the AudioContext does not start, because it has been explicitly suspended -`); + await createAndStartAudioScheduledSourceNode(nodeType); + } + + SimpleTest.finish(); +})(); + +/** + * Test utility 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], + ]}); +} + +async function createAudioContext() { + /* global ac */ + window.ac = new AudioContext(); + await once(ac, "blocked"); + is(ac.state, "suspended", `AudioContext is blocked.`); +} + +function suspendAudioContext() { + try { + ac.suspend(); + } catch(e) { + ok(false, `AudioContext suspend failed!`); + } +} + +async function createAndStartAudioScheduledSourceNode(nodeType) { + let node; + info(`- create ${nodeType} -`); + switch (nodeType) { + 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); + + // activate the document in order to allow autoplay. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + node.start(); + + await once(ac, "blocked"); + is(ac.state, "suspended", `AudioContext should not be resumed.`); + + // reset the activation flag of the document in order not to interfere next test. + SpecialPowers.wrap(document).clearUserGestureActivation(); +} + +</script> 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 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test that a MediaStream source triggers autoplay</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +/* import-globals-from ../../../test/manifest.js */ +SimpleTest.waitForExplicitFinish(); + +var media = getPlayableVideo(gSmallTests); + +if (media == null) { + todo(false, "No media supported."); + SimpleTest.finish(); +} else { + function startTest() { + var v1 = document.createElement('video'); + var v2 = document.createElement('video'); + v1.preload = 'metadata'; + v2.autoplay = true; + document.body.appendChild(v1); + document.body.appendChild(v2); + + v1.src = media.name; + v1.onloadedmetadata = function() { + v2.srcObject = v1.mozCaptureStream(); + v1.play(); + }; + + v2.addEventListener('playing', function() { + ok(true, "playback started"); + SimpleTest.finish(); + }, {once: true}); + } + + startTest(); +} + +</script> +</pre> +</body> +</html> |