summaryrefslogtreecommitdiffstats
path: root/dom/media/autoplay
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/media/autoplay
parentInitial commit. (diff)
downloadfirefox-esr-upstream/115.8.0esr.tar.xz
firefox-esr-upstream/115.8.0esr.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/autoplay')
-rw-r--r--dom/media/autoplay/AutoplayPolicy.cpp497
-rw-r--r--dom/media/autoplay/AutoplayPolicy.h80
-rw-r--r--dom/media/autoplay/GVAutoplayPermissionRequest.cpp236
-rw-r--r--dom/media/autoplay/GVAutoplayPermissionRequest.h86
-rw-r--r--dom/media/autoplay/GVAutoplayRequestStatusIPC.h23
-rw-r--r--dom/media/autoplay/GVAutoplayRequestUtils.h25
-rw-r--r--dom/media/autoplay/moz.build32
-rw-r--r--dom/media/autoplay/nsIAutoplay.idl17
-rw-r--r--dom/media/autoplay/test/browser/audio.oggbin0 -> 14290 bytes
-rw-r--r--dom/media/autoplay/test/browser/browser.ini27
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_detection_click_to_play.js120
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_and_site_sticky.js168
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_sticky.js105
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_play_twice.js54
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_request_permission.js269
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_touchScroll.js103
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_user_gestures.js277
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_webRTC_permission.js67
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio.js217
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio_with_gum.js174
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_videoDocument.js80
-rw-r--r--dom/media/autoplay/test/browser/file_empty.html8
-rw-r--r--dom/media/autoplay/test/browser/file_mediaplayback_frame.html21
-rw-r--r--dom/media/autoplay/test/browser/file_nonAutoplayAudio.html7
-rw-r--r--dom/media/autoplay/test/browser/file_video.html9
-rw-r--r--dom/media/autoplay/test/browser/head.js149
-rw-r--r--dom/media/autoplay/test/mochitest/AutoplayTestUtils.js46
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_frame.html24
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_window.html65
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_frame.html32
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_window.html80
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_policy_eventdown_activation.html85
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html148
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_policy_play_before_loadedmetadata.html63
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_policy_unmute_pauses.html65
-rw-r--r--dom/media/autoplay/test/mochitest/mochitest.ini54
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay.html36
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_contentEditable.html67
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_gv_play_request.html221
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy.html174
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_activation.html180
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_eventdown_activation.html55
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_key_blacklist.html47
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_permission.html80
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_play_before_loadedmetadata.html73
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_unmute_pauses.html64
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_AudioParamStream.html171
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_createMediaStreamSource.html119
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html105
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html96
-rw-r--r--dom/media/autoplay/test/mochitest/test_streams_autoplay.html47
51 files changed, 5048 insertions, 0 deletions
diff --git a/dom/media/autoplay/AutoplayPolicy.cpp b/dom/media/autoplay/AutoplayPolicy.cpp
new file mode 100644
index 0000000000..8da0e60ead
--- /dev/null
+++ b/dom/media/autoplay/AutoplayPolicy.cpp
@@ -0,0 +1,497 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AutoplayPolicy.h"
+
+#include "mozilla/dom/AudioContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/FeaturePolicyUtils.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/HTMLMediaElementBinding.h"
+#include "mozilla/dom/NavigatorBinding.h"
+#include "mozilla/dom/UserActivation.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MediaManager.h"
+#include "mozilla/Components.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsContentUtils.h"
+#include "nsGlobalWindowInner.h"
+#include "nsIAutoplay.h"
+#include "nsIDocShell.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIPermissionManager.h"
+#include "nsIPrincipal.h"
+#include "nsPIDOMWindow.h"
+
+mozilla::LazyLogModule gAutoplayPermissionLog("Autoplay");
+
+#define AUTOPLAY_LOG(msg, ...) \
+ MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+using namespace mozilla::dom;
+
+namespace mozilla::media {
+
+static const uint32_t sPOLICY_STICKY_ACTIVATION = 0;
+// static const uint32_t sPOLICY_TRANSIENT_ACTIVATION = 1;
+static const uint32_t sPOLICY_USER_INPUT_DEPTH = 2;
+
+static bool IsActivelyCapturingOrHasAPermission(nsPIDOMWindowInner* aWindow) {
+ // Pages which have been granted permission to capture WebRTC camera or
+ // microphone or screen are assumed to be trusted, and are allowed to
+ // autoplay.
+ if (MediaManager::GetIfExists()) {
+ return MediaManager::GetIfExists()->IsActivelyCapturingOrHasAPermission(
+ aWindow->WindowID());
+ }
+
+ auto principal = nsGlobalWindowInner::Cast(aWindow)->GetPrincipal();
+ return (nsContentUtils::IsExactSitePermAllow(principal, "camera"_ns) ||
+ nsContentUtils::IsExactSitePermAllow(principal, "microphone"_ns) ||
+ nsContentUtils::IsExactSitePermAllow(principal, "screen"_ns));
+}
+
+static uint32_t SiteAutoplayPerm(nsPIDOMWindowInner* aWindow) {
+ if (!aWindow || !aWindow->GetBrowsingContext()) {
+ return nsIPermissionManager::UNKNOWN_ACTION;
+ }
+
+ WindowContext* topContext =
+ aWindow->GetBrowsingContext()->GetTopWindowContext();
+ if (!topContext) {
+ return nsIPermissionManager::UNKNOWN_ACTION;
+ }
+ return topContext->GetAutoplayPermission();
+}
+
+static bool IsWindowAllowedToPlayByUserGesture(nsPIDOMWindowInner* aWindow) {
+ if (!aWindow) {
+ return false;
+ }
+
+ WindowContext* topContext =
+ aWindow->GetBrowsingContext()->GetTopWindowContext();
+ if (topContext && topContext->HasBeenUserGestureActivated()) {
+ AUTOPLAY_LOG(
+ "Allow autoplay as top-level context has been activated by user "
+ "gesture.");
+ return true;
+ }
+ return false;
+}
+
+static bool IsWindowAllowedToPlayByTraits(nsPIDOMWindowInner* aWindow) {
+ if (!aWindow) {
+ return false;
+ }
+
+ if (IsActivelyCapturingOrHasAPermission(aWindow)) {
+ AUTOPLAY_LOG(
+ "Allow autoplay as document has camera or microphone or screen"
+ " permission.");
+ return true;
+ }
+
+ Document* currentDoc = aWindow->GetExtantDoc();
+ if (!currentDoc) {
+ return false;
+ }
+
+ bool isTopLevelContent = !aWindow->GetBrowsingContext()->GetParent();
+ if (currentDoc->MediaDocumentKind() == Document::MediaDocumentKind::Video &&
+ isTopLevelContent) {
+ AUTOPLAY_LOG("Allow top-level video document to autoplay.");
+ return true;
+ }
+
+ if (StaticPrefs::media_autoplay_allow_extension_background_pages() &&
+ currentDoc->IsExtensionPage()) {
+ AUTOPLAY_LOG("Allow autoplay as in extension document.");
+ return true;
+ }
+
+ return false;
+}
+
+static bool IsWindowAllowedToPlayOverall(nsPIDOMWindowInner* aWindow) {
+ return IsWindowAllowedToPlayByUserGesture(aWindow) ||
+ IsWindowAllowedToPlayByTraits(aWindow);
+}
+
+static uint32_t DefaultAutoplayBehaviour() {
+ int32_t prefValue = StaticPrefs::media_autoplay_default();
+ if (prefValue == nsIAutoplay::ALLOWED) {
+ return nsIAutoplay::ALLOWED;
+ }
+ if (prefValue == nsIAutoplay::BLOCKED_ALL) {
+ return nsIAutoplay::BLOCKED_ALL;
+ }
+ return nsIAutoplay::BLOCKED;
+}
+
+static bool IsMediaElementInaudible(const HTMLMediaElement& aElement) {
+ if (aElement.Volume() == 0.0 || aElement.Muted()) {
+ AUTOPLAY_LOG("Media %p is muted.", &aElement);
+ return true;
+ }
+
+ if (!aElement.HasAudio() &&
+ aElement.ReadyState() >= HTMLMediaElement_Binding::HAVE_METADATA) {
+ AUTOPLAY_LOG("Media %p has no audio track", &aElement);
+ return true;
+ }
+
+ return false;
+}
+
+static bool IsAudioContextAllowedToPlay(const AudioContext& aContext) {
+ // Offline context won't directly output sound to audio devices.
+ return aContext.IsOffline() ||
+ IsWindowAllowedToPlayOverall(aContext.GetParentObject());
+}
+
+static bool IsEnableBlockingWebAudioByUserGesturePolicy() {
+ return StaticPrefs::media_autoplay_block_webaudio() &&
+ StaticPrefs::media_autoplay_blocking_policy() ==
+ sPOLICY_STICKY_ACTIVATION;
+}
+
+static bool IsAllowedToPlayByBlockingModel(const HTMLMediaElement& aElement) {
+ const uint32_t policy = StaticPrefs::media_autoplay_blocking_policy();
+ if (policy == sPOLICY_STICKY_ACTIVATION) {
+ const bool isAllowed =
+ IsWindowAllowedToPlayOverall(aElement.OwnerDoc()->GetInnerWindow());
+ AUTOPLAY_LOG("Use 'sticky-activation', isAllowed=%d", isAllowed);
+ return isAllowed;
+ }
+ // If element is blessed, it would always be allowed to play().
+ const bool isElementBlessed = aElement.IsBlessed();
+ if (policy == sPOLICY_USER_INPUT_DEPTH) {
+ const bool isUserInput = UserActivation::IsHandlingUserInput();
+ AUTOPLAY_LOG("Use 'User-Input-Depth', isBlessed=%d, isUserInput=%d",
+ isElementBlessed, isUserInput);
+ return isElementBlessed || isUserInput;
+ }
+ const bool hasTransientActivation =
+ aElement.OwnerDoc()->HasValidTransientUserGestureActivation();
+ AUTOPLAY_LOG(
+ "Use 'transient-activation', isBlessed=%d, "
+ "hasValidTransientActivation=%d",
+ isElementBlessed, hasTransientActivation);
+ return isElementBlessed || hasTransientActivation;
+}
+
+// On GeckoView, we don't store any site's permission in permission manager, we
+// would check the GV request status to know if the site can be allowed to play.
+// But on other platforms, we would store the site's permission in permission
+// manager.
+#if defined(MOZ_WIDGET_ANDROID)
+using RType = GVAutoplayRequestType;
+
+static bool IsGVAutoplayRequestAllowed(nsPIDOMWindowInner* aWindow,
+ RType aType) {
+ if (!aWindow) {
+ return false;
+ }
+
+ RefPtr<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..600c7d291e
--- /dev/null
+++ b/dom/media/autoplay/GVAutoplayPermissionRequest.cpp
@@ -0,0 +1,236 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GVAutoplayPermissionRequest.h"
+
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsGlobalWindowInner.h"
+
+mozilla::LazyLogModule gGVAutoplayRequestLog("GVAutoplay");
+
+namespace mozilla::dom {
+
+using RType = GVAutoplayRequestType;
+using RStatus = GVAutoplayRequestStatus;
+
+const char* ToGVRequestTypeStr(RType aType) {
+ switch (aType) {
+ case RType::eINAUDIBLE:
+ return "inaudible";
+ case RType::eAUDIBLE:
+ return "audible";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid request type.");
+ return "invalid";
+ }
+}
+
+const char* ToGVRequestStatusStr(RStatus aStatus) {
+ switch (aStatus) {
+ case RStatus::eUNKNOWN:
+ return "Unknown";
+ case RStatus::eALLOWED:
+ return "Allowed";
+ case RStatus::eDENIED:
+ return "Denied";
+ case RStatus::ePENDING:
+ return "Pending";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid status.");
+ return "Invalid";
+ }
+}
+
+// avoid redefined macro in unified build
+#undef REQUEST_LOG
+#define REQUEST_LOG(msg, ...) \
+ if (MOZ_LOG_TEST(gGVAutoplayRequestLog, mozilla::LogLevel::Debug)) { \
+ MOZ_LOG(gGVAutoplayRequestLog, LogLevel::Debug, \
+ ("Request=%p, Type=%s, " msg, this, \
+ ToGVRequestTypeStr(this->mType), ##__VA_ARGS__)); \
+ }
+
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gGVAutoplayRequestLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+static RStatus GetRequestStatus(BrowsingContext* aContext, RType aType) {
+ MOZ_ASSERT(aContext);
+ AssertIsOnMainThread();
+ return aType == RType::eAUDIBLE
+ ? aContext->GetGVAudibleAutoplayRequestStatus()
+ : aContext->GetGVInaudibleAutoplayRequestStatus();
+}
+
+// This is copied from the value of `media.geckoview.autoplay.request.testing`.
+enum class TestRequest : uint32_t {
+ ePromptAsNormal = 0,
+ eAllowAll = 1,
+ eDenyAll = 2,
+ eAllowAudible = 3,
+ eDenyAudible = 4,
+ eAllowInAudible = 5,
+ eDenyInAudible = 6,
+ eLeaveAllPending = 7,
+};
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(GVAutoplayPermissionRequest,
+ ContentPermissionRequestBase)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(GVAutoplayPermissionRequest,
+ ContentPermissionRequestBase)
+
+/* static */
+void GVAutoplayPermissionRequest::CreateRequest(nsGlobalWindowInner* aWindow,
+ BrowsingContext* aContext,
+ GVAutoplayRequestType aType) {
+ RefPtr<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->EventTargetFor(TaskCategory::Other),
+ GVAutoplayPermissionRequest::DelayedTaskType::Request);
+ }
+}
+
+GVAutoplayPermissionRequest::GVAutoplayPermissionRequest(
+ nsGlobalWindowInner* aWindow, BrowsingContext* aContext, RType aType)
+ : ContentPermissionRequestBase(aWindow->GetPrincipal(), aWindow,
+ ""_ns, // No testing pref used in this class
+ aType == RType::eAUDIBLE
+ ? "autoplay-media-audible"_ns
+ : "autoplay-media-inaudible"_ns),
+ mType(aType),
+ mContext(aContext) {
+ MOZ_ASSERT(mContext);
+ REQUEST_LOG("Request created");
+}
+
+GVAutoplayPermissionRequest::~GVAutoplayPermissionRequest() {
+ REQUEST_LOG("Request destroyed");
+ // If user doesn't response to the request before it gets destroyed (ex.
+ // request dismissed, tab closed, naviagation to a new page), then we should
+ // treat it as a denial.
+ if (mContext) {
+ Cancel();
+ }
+}
+
+void GVAutoplayPermissionRequest::SetRequestStatus(RStatus aStatus) {
+ REQUEST_LOG("SetRequestStatus, new status=%s", ToGVRequestStatusStr(aStatus));
+ MOZ_ASSERT(mContext);
+ AssertIsOnMainThread();
+ if (mType == RType::eAUDIBLE) {
+ // Return value of setting synced field should be checked. See bug 1656492.
+ Unused << mContext->SetGVAudibleAutoplayRequestStatus(aStatus);
+ } else {
+ // Return value of setting synced field should be checked. See bug 1656492.
+ Unused << mContext->SetGVInaudibleAutoplayRequestStatus(aStatus);
+ }
+}
+
+NS_IMETHODIMP
+GVAutoplayPermissionRequest::Cancel() {
+ MOZ_ASSERT(mContext, "Do not call 'Cancel()' twice!");
+ // As the process of replying of the request is an async task, the status
+ // might have be reset at the time we get the result from parent process.
+ // Ex. if the page got closed or naviagated immediately after user replied to
+ // the request. Therefore, the status should be either `pending` or `unknown`.
+ const RStatus status = GetRequestStatus(mContext, mType);
+ REQUEST_LOG("Cancel, current status=%s", ToGVRequestStatusStr(status));
+ MOZ_ASSERT(status == RStatus::ePENDING || status == RStatus::eUNKNOWN);
+ if ((status == RStatus::ePENDING) && !mContext->IsDiscarded()) {
+ SetRequestStatus(RStatus::eDENIED);
+ }
+ mContext = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GVAutoplayPermissionRequest::Allow(JS::Handle<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..338f0798f8
--- /dev/null
+++ b/dom/media/autoplay/moz.build
@@ -0,0 +1,32 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("test/**"):
+ BUG_COMPONENT = ("Core", "Audio/Video: Playback")
+
+EXPORTS += [
+ "AutoplayPolicy.h",
+ "GVAutoplayPermissionRequest.h",
+ "GVAutoplayRequestStatusIPC.h",
+ "GVAutoplayRequestUtils.h",
+]
+
+UNIFIED_SOURCES += [
+ "AutoplayPolicy.cpp",
+ "GVAutoplayPermissionRequest.cpp",
+]
+
+XPIDL_MODULE = "autoplay"
+XPIDL_SOURCES += [
+ "nsIAutoplay.idl",
+]
+
+MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.ini"]
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/autoplay/nsIAutoplay.idl b/dom/media/autoplay/nsIAutoplay.idl
new file mode 100644
index 0000000000..f18c72ada9
--- /dev/null
+++ b/dom/media/autoplay/nsIAutoplay.idl
@@ -0,0 +1,17 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(048a24f6-c4d6-47bc-bea2-f6038d1db80a)]
+interface nsIAutoplay : nsISupports
+{
+ /*
+ * Possible values for the "media.autoplay.default" preference.
+ */
+ const uint32_t ALLOWED = 0;
+ const uint32_t BLOCKED = 1;
+ const uint32_t BLOCKED_ALL = 5;
+};
diff --git a/dom/media/autoplay/test/browser/audio.ogg b/dom/media/autoplay/test/browser/audio.ogg
new file mode 100644
index 0000000000..7f1833508a
--- /dev/null
+++ b/dom/media/autoplay/test/browser/audio.ogg
Binary files differ
diff --git a/dom/media/autoplay/test/browser/browser.ini b/dom/media/autoplay/test/browser/browser.ini
new file mode 100644
index 0000000000..8bdf83859e
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser.ini
@@ -0,0 +1,27 @@
+[DEFAULT]
+subsuite = media-bc
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573
+tags = autoplay
+support-files =
+ ../../../test/gizmo.mp4
+ audio.ogg
+ file_empty.html
+ file_mediaplayback_frame.html
+ file_nonAutoplayAudio.html
+ file_video.html
+ head.js
+
+[browser_autoplay_policy_detection_global_sticky.js]
+[browser_autoplay_policy_detection_global_and_site_sticky.js]
+[browser_autoplay_policy_detection_click_to_play.js]
+[browser_autoplay_policy_play_twice.js]
+[browser_autoplay_policy_user_gestures.js]
+https_first_disabled = true
+[browser_autoplay_policy_request_permission.js]
+https_first_disabled = true
+[browser_autoplay_policy_touchScroll.js]
+https_first_disabled = true
+[browser_autoplay_policy_web_audio.js]
+[browser_autoplay_policy_web_audio_with_gum.js]
+[browser_autoplay_policy_webRTC_permission.js]
+[browser_autoplay_videoDocument.js]
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_click_to_play.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_click_to_play.js
new file mode 100644
index 0000000000..576f01b1cf
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_click_to_play.js
@@ -0,0 +1,120 @@
+/**
+ * This test will check the Autoplay Policy Detection API for click-to-play
+ * blocking policy (media.autoplay.blocking_policy=2) and the blocked value set
+ * to BLOCKED (block audible) and BLOCKED_ALL (block audible & inaudible).
+ *
+ * We will create two video elements in the test page, and then click one of
+ * them. After doing that, only the element has been clicked can be allowed to
+ * autoplay, other elements should remain blocked depend on the default blocking
+ * value.
+ */
+"use strict";
+
+// TODO : remove this when it's enabled by default in bug 1812189.
+add_setup(async function setSharedPrefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.media.autoplay-policy-detection.enabled", true]],
+ });
+});
+
+async function testAutoplayPolicy(defaultPolicy) {
+ await setupTestPref(defaultPolicy);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ await createVideoElements(tab);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [defaultPolicy],
+ defaultPolicy => {
+ is(
+ content.navigator.getAutoplayPolicy("mediaelement"),
+ defaultPolicy,
+ "Check autoplay policy by media element type is correct"
+ );
+ let videos = content.document.getElementsByTagName("video");
+ for (let video of videos) {
+ is(
+ content.navigator.getAutoplayPolicy(video),
+ defaultPolicy,
+ "Check autoplay policy by element is correct"
+ );
+ }
+ }
+ );
+
+ info("click on one video to make it play");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#will-be-clicked",
+ { button: 0 },
+ tab.linkedBrowser
+ );
+
+ info("only the element has been clicked can be allowed to autoplay");
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [defaultPolicy],
+ defaultPolicy => {
+ is(
+ content.navigator.getAutoplayPolicy("mediaelement"),
+ defaultPolicy,
+ "Check autoplay policy by media element type is correct"
+ );
+ let videos = content.document.getElementsByTagName("video");
+ for (let video of videos) {
+ is(
+ content.navigator.getAutoplayPolicy(video),
+ video.id === "will-be-clicked" ? "allowed" : defaultPolicy,
+ "Check autoplay policy by element is correct"
+ );
+ }
+ }
+ );
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function testAutoplayPolicyDetectionForClickToPlay() {
+ await testAutoplayPolicy("allowed-muted");
+ await testAutoplayPolicy("disallowed");
+});
+
+// Following are helper functions
+async function setupTestPref(defaultPolicy) {
+ function policyToBlockedValue(defaultPolicy) {
+ // Value for media.autoplay.default
+ if (defaultPolicy === "allowed") {
+ return 0 /* Allowed */;
+ } else if (defaultPolicy === "allowed-muted") {
+ return 1 /* Blocked */;
+ }
+ return 5 /* Blocked All */;
+ }
+ const defaultBlocked = policyToBlockedValue(defaultPolicy);
+ info(`Set 'media.autoplay.default' to ${defaultBlocked}`);
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", defaultBlocked],
+ ["media.autoplay.blocking_policy", 2 /* click-to-play */],
+ ],
+ });
+}
+
+function createVideoElements(tab) {
+ info("create two video elements in the page");
+ let url = GetTestWebBasedURL("gizmo.mp4");
+ return SpecialPowers.spawn(tab.linkedBrowser, [url], url => {
+ let video1 = content.document.createElement("video");
+ video1.id = "will-be-clicked";
+ video1.controls = true;
+ video1.src = url;
+
+ let video2 = content.document.createElement("video");
+ video2.controls = true;
+ video2.src = url;
+
+ content.document.body.appendChild(video1);
+ content.document.body.appendChild(video2);
+ });
+}
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_and_site_sticky.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_and_site_sticky.js
new file mode 100644
index 0000000000..828b3e0986
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_and_site_sticky.js
@@ -0,0 +1,168 @@
+/**
+ * This test checks whether Autoplay Policy Detection API works correctly under
+ * different situations of having global permission set for block autoplay
+ * along with different site permission setting. This test only checks the
+ * sticky user gesture blocking model.
+ */
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+// We can't set site permission on 'about:blank' so we use an empty page.
+const PAGE_URL = GetTestWebBasedURL("file_empty.html");
+
+add_setup(async function setSharedPrefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.media.autoplay-policy-detection.enabled", true],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-webaudio", true],
+ ],
+ });
+});
+
+add_task(async function testGlobalPermissionIsAllowed() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.ALLOWED]],
+ });
+ let tab = await createTabAndSetupPolicyAssertFunc(PAGE_URL);
+ PermissionTestUtils.add(
+ tab.linkedBrowser.currentURI,
+ "autoplay-media",
+ Services.perms.DENY_ACTION
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("site permission blocks audible autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed-muted",
+ resultForElement: "allowed-muted",
+ resultForContextType: "disallowed",
+ resultForContext: "disallowed",
+ });
+ });
+ PermissionTestUtils.add(
+ tab.linkedBrowser.currentURI,
+ "autoplay-media",
+ Ci.nsIAutoplay.BLOCKED_ALL
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("site permission blocks all autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "disallowed",
+ resultForElement: "disallowed",
+ resultForContextType: "disallowed",
+ resultForContext: "disallowed",
+ });
+
+ info(
+ "activate document by using user gesture, all autoplay will be allowed"
+ );
+ content.document.notifyUserGestureActivation();
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ PermissionTestUtils.remove(tab.linkedBrowser.currentURI, "autoplay-media");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testGlobalPermissionIsBlocked() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED]],
+ });
+ let tab = await createTabAndSetupPolicyAssertFunc(PAGE_URL);
+ PermissionTestUtils.add(
+ tab.linkedBrowser.currentURI,
+ "autoplay-media",
+ Services.perms.ALLOW_ACTION
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("site permission allows all autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ PermissionTestUtils.add(
+ tab.linkedBrowser.currentURI,
+ "autoplay-media",
+ Ci.nsIAutoplay.BLOCKED_ALL
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("site permission blocks all autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "disallowed",
+ resultForElement: "disallowed",
+ resultForContextType: "disallowed",
+ resultForContext: "disallowed",
+ });
+
+ info(
+ "activate document by using user gesture, all autoplay will be allowed"
+ );
+ content.document.notifyUserGestureActivation();
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ PermissionTestUtils.remove(tab.linkedBrowser.currentURI, "autoplay-media");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testGlobalPermissionIsBlockedAll() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED_ALL]],
+ });
+ let tab = await createTabAndSetupPolicyAssertFunc(PAGE_URL);
+ PermissionTestUtils.add(
+ tab.linkedBrowser.currentURI,
+ "autoplay-media",
+ Services.perms.ALLOW_ACTION
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("site permission allows all autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ PermissionTestUtils.add(
+ tab.linkedBrowser.currentURI,
+ "autoplay-media",
+ Services.perms.DENY_ACTION
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("site permission blocks audible autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed-muted",
+ resultForElement: "allowed-muted",
+ resultForContextType: "disallowed",
+ resultForContext: "disallowed",
+ });
+
+ info(
+ "activate document by using user gesture, all autoplay will be allowed"
+ );
+ content.document.notifyUserGestureActivation();
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ PermissionTestUtils.remove(tab.linkedBrowser.currentURI, "autoplay-media");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_sticky.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_sticky.js
new file mode 100644
index 0000000000..ab9fe6b418
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_sticky.js
@@ -0,0 +1,105 @@
+/**
+ * This test checks whether Autoplay Policy Detection API works correctly under
+ * different situations of having global permission set for block autoplay.
+ * This test only checks the sticky user gesture blocking model.
+ */
+"use strict";
+
+add_setup(async function setSharedPrefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.media.autoplay-policy-detection.enabled", true],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-webaudio", true],
+ ],
+ });
+});
+
+add_task(async function testGlobalPermissionIsAllowed() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.ALLOWED]],
+ });
+ let tab = await createTabAndSetupPolicyAssertFunc("about:blank");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("global setting allows any autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testGlobalPermissionIsBlocked() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED]],
+ });
+ let tab = await createTabAndSetupPolicyAssertFunc("about:blank");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info(
+ "global setting allows inaudible autoplay but audible autoplay is still not allowed"
+ );
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed-muted",
+ resultForElement: "allowed-muted",
+ resultForContextType: "disallowed",
+ resultForContext: "disallowed",
+ });
+
+ info("tweaking video's muted attribute won't change the result");
+ content.video.muted = true;
+ is(
+ "allowed-muted",
+ content.navigator.getAutoplayPolicy(content.video),
+ "getAutoplayPolicy(video) returns correct value"
+ );
+ content.video.muted = false;
+ is(
+ "allowed-muted",
+ content.navigator.getAutoplayPolicy(content.video),
+ "getAutoplayPolicy(video) returns correct value"
+ );
+
+ info(
+ "activate document by using user gesture, all autoplay will be allowed"
+ );
+ content.document.notifyUserGestureActivation();
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testGlobalPermissionIsBlockedAll() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED_ALL]],
+ });
+ let tab = await createTabAndSetupPolicyAssertFunc("about:blank");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("global setting doesn't allow any autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "disallowed",
+ resultForElement: "disallowed",
+ resultForContextType: "disallowed",
+ resultForContext: "disallowed",
+ });
+
+ info(
+ "activate document by using user gesture, all autoplay will be allowed"
+ );
+ content.document.notifyUserGestureActivation();
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_play_twice.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_play_twice.js
new file mode 100644
index 0000000000..7130e6e781
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_play_twice.js
@@ -0,0 +1,54 @@
+const VIDEO_PAGE = GetTestWebBasedURL("file_video.html");
+
+function setup_test_preference(enableUserGesture) {
+ let state = enableUserGesture ? "enable" : "disable";
+ info(`- set pref : ${state} user gesture -`);
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", enableUserGesture ? 0 : 1],
+ ],
+ });
+}
+
+async function allow_play_for_played_video() {
+ info("- open new tab -");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, VIDEO_PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- simulate user-click to start video -");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#v",
+ { button: 0 },
+ tab.linkedBrowser
+ );
+
+ async function play_video_again() {
+ let video = content.document.getElementById("v");
+ ok(!video.paused, "video is playing");
+
+ info("- call video play() again -");
+ try {
+ await video.play();
+ ok(true, "success to resolve play promise");
+ } catch (e) {
+ ok(false, "promise should not be rejected");
+ }
+ }
+ await SpecialPowers.spawn(tab.linkedBrowser, [], play_video_again);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function start_test() {
+ await setup_test_preference(true);
+ await allow_play_for_played_video();
+
+ await setup_test_preference(false);
+ await allow_play_for_played_video();
+});
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_request_permission.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_request_permission.js
new file mode 100644
index 0000000000..5cdb3ffe6c
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_request_permission.js
@@ -0,0 +1,269 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const VIDEO_PAGE_URI = GetTestWebBasedURL("file_empty.html");
+const SAME_ORIGIN_FRAME_URI = GetTestWebBasedURL(
+ "file_mediaplayback_frame.html"
+);
+const DIFFERENT_ORIGIN_FRAME_URI = GetTestWebBasedURL(
+ "file_mediaplayback_frame.html",
+ { crossOrigin: true }
+);
+
+const gPermissionName = "autoplay-media";
+
+function setTestingPreferences(defaultSetting) {
+ info(`set default autoplay setting to '${defaultSetting}'`);
+ let defaultValue =
+ defaultSetting == "blocked"
+ ? SpecialPowers.Ci.nsIAutoplay.BLOCKED
+ : SpecialPowers.Ci.nsIAutoplay.ALLOWED;
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", defaultValue],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-event.enabled", true],
+ ],
+ });
+}
+
+async function testAutoplayExistingPermission(args) {
+ info("- Starting '" + args.name + "' -");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: VIDEO_PAGE_URI,
+ },
+ async browser => {
+ let promptShowing = () =>
+ PopupNotifications.getNotification("autoplay-media", browser);
+
+ PermissionTestUtils.add(
+ browser.currentURI,
+ "autoplay-media",
+ args.permission
+ );
+ ok(!promptShowing(), "Should not be showing permission prompt yet");
+
+ await loadAutoplayVideo(browser, args);
+ await checkVideoDidPlay(browser, args);
+
+ // Reset permission.
+ PermissionTestUtils.remove(browser.currentURI, "autoplay-media");
+
+ info("- Finished '" + args.name + "' -");
+ }
+ );
+}
+
+async function testAutoplayExistingPermissionAgainstDefaultSetting(args) {
+ await setTestingPreferences(args.defaultSetting);
+ await testAutoplayExistingPermission(args);
+}
+
+// Test the simple ALLOW/BLOCK cases; when permission is already set to ALLOW,
+// we shoud be able to autoplay via calling play(), or via the autoplay attribute,
+// and when it's set to BLOCK, we should not.
+add_task(async () => {
+ await setTestingPreferences("blocked" /* default setting */);
+ await testAutoplayExistingPermission({
+ name: "Prexisting allow permission autoplay attribute",
+ permission: Services.perms.ALLOW_ACTION,
+ shouldPlay: true,
+ mode: "autoplay attribute",
+ });
+ await testAutoplayExistingPermission({
+ name: "Prexisting allow permission call play",
+ permission: Services.perms.ALLOW_ACTION,
+ shouldPlay: true,
+ mode: "call play",
+ });
+ await testAutoplayExistingPermission({
+ name: "Prexisting block permission autoplay attribute",
+ permission: Services.perms.DENY_ACTION,
+ shouldPlay: false,
+ mode: "autoplay attribute",
+ });
+ await testAutoplayExistingPermission({
+ name: "Prexisting block permission call play",
+ permission: Services.perms.DENY_ACTION,
+ shouldPlay: false,
+ mode: "call play",
+ });
+});
+
+/**
+ * These tests are used to ensure the autoplay setting for specific site can
+ * always override the default autoplay setting.
+ */
+add_task(async () => {
+ await testAutoplayExistingPermissionAgainstDefaultSetting({
+ name: "Site has prexisting allow permission but default setting is 'blocked'",
+ permission: Services.perms.ALLOW_ACTION,
+ defaultSetting: "blocked",
+ shouldPlay: true,
+ mode: "autoplay attribute",
+ });
+ await testAutoplayExistingPermissionAgainstDefaultSetting({
+ name: "Site has prexisting block permission but default setting is 'allowed'",
+ permission: Services.perms.DENY_ACTION,
+ defaultSetting: "allowed",
+ shouldPlay: false,
+ mode: "autoplay attribute",
+ });
+});
+
+/**
+ * The permission of the main page's domain would determine the final autoplay
+ * result when a page contains multiple iframes which are in the different
+ * domain from the main pages's.
+ * That means we would not check the permission of iframe's domain, even if it
+ * has been set.
+ */
+add_task(async function testExistingPermissionForIframe() {
+ await setTestingPreferences("blocked" /* default setting */);
+ await testAutoplayExistingPermissionForIframe({
+ name: "Prexisting ALLOW for main page with same origin iframe",
+ permissionForParent: Services.perms.ALLOW_ACTION,
+ isIframeDifferentOrgin: true,
+ shouldPlay: true,
+ });
+
+ await testAutoplayExistingPermissionForIframe({
+ name: "Prexisting ALLOW for main page with different origin iframe",
+ permissionForParent: Services.perms.ALLOW_ACTION,
+ isIframeDifferentOrgin: false,
+ shouldPlay: true,
+ });
+
+ await testAutoplayExistingPermissionForIframe({
+ name: "Prexisting ALLOW for main page, prexisting DENY for different origin iframe",
+ permissionForParent: Services.perms.ALLOW_ACTION,
+ permissionForChild: Services.perms.DENY_ACTION,
+ isIframeDifferentOrgin: false,
+ shouldPlay: true,
+ });
+
+ await testAutoplayExistingPermissionForIframe({
+ name: "Prexisting DENY for main page, prexisting ALLOW for different origin iframe",
+ permissionForParent: Services.perms.DENY_ACTION,
+ permissionForChild: Services.perms.ALLOW_ACTION,
+ isIframeDifferentOrgin: false,
+ shouldPlay: false,
+ });
+});
+
+/**
+ * The following are helper functions.
+ */
+async function testAutoplayExistingPermissionForIframe(args) {
+ info(`Start test : ${args.name}`);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: VIDEO_PAGE_URI,
+ },
+ async browser => {
+ setupSitesPermission(browser, args);
+
+ await createIframe(browser, args);
+ await checkAutplayInIframe(browser, args);
+
+ clearSitesPermission(browser, args);
+ }
+ );
+ info(`Finish test : ${args.name}`);
+}
+
+function setupSitesPermission(
+ browser,
+ {
+ isIframeDifferentOrgin,
+ permissionForParent,
+ permissionForChild = Services.perms.UNKNOWN_ACTION,
+ }
+) {
+ info(`setupSitesPermission`);
+ // Set permission for the main page's domain
+ setPermissionForBrowser(browser, browser.currentURI, permissionForParent);
+ if (isIframeDifferentOrgin) {
+ // Set permission for different domain of the iframe
+ setPermissionForBrowser(
+ browser,
+ DIFFERENT_ORIGIN_FRAME_URI,
+ permissionForChild
+ );
+ }
+}
+
+function clearSitesPermission(browser, { isIframeDifferentOrgin }) {
+ info(`clearSitesPermission`);
+ // Clear permission for the main page's domain
+ setPermissionForBrowser(
+ browser,
+ browser.currentURI,
+ Services.perms.UNKNOWN_ACTION
+ );
+ if (isIframeDifferentOrgin) {
+ // Clear permission for different domain of the iframe
+ setPermissionForBrowser(
+ browser,
+ DIFFERENT_ORIGIN_FRAME_URI,
+ Services.perms.UNKNOWN_ACTION
+ );
+ }
+}
+
+function setPermissionForBrowser(browser, uri, permValue) {
+ const promptShowing = () =>
+ PopupNotifications.getNotification(gPermissionName, browser);
+ PermissionTestUtils.add(uri, gPermissionName, permValue);
+ ok(!promptShowing(), "Should not be showing permission prompt yet");
+ is(
+ PermissionTestUtils.testExactPermission(uri, gPermissionName),
+ permValue,
+ "Set permission correctly"
+ );
+}
+
+function createIframe(browser, { isIframeDifferentOrgin }) {
+ const iframeURL = isIframeDifferentOrgin
+ ? DIFFERENT_ORIGIN_FRAME_URI
+ : SAME_ORIGIN_FRAME_URI;
+ return SpecialPowers.spawn(browser, [iframeURL], async url => {
+ info(`Create iframe and wait until it finsihes loading`);
+ const iframe = content.document.createElement("iframe");
+ iframe.src = url;
+ content.document.body.appendChild(iframe);
+ await new Promise(r => (iframe.onload = r));
+ });
+}
+
+function checkAutplayInIframe(browser, args) {
+ return SpecialPowers.spawn(browser, [args], async ({ shouldPlay }) => {
+ info(`check if media in iframe can start playing`);
+ const iframe = content.document.getElementsByTagName("iframe")[0];
+ if (!iframe) {
+ ok(false, `can not get the iframe!`);
+ return;
+ }
+ iframe.contentWindow.postMessage("play", "*");
+ await new Promise(r => {
+ content.onmessage = event => {
+ if (shouldPlay) {
+ is(event.data, "played", `played media in iframe`);
+ } else {
+ is(event.data, "blocked", `blocked media in iframe`);
+ }
+ r();
+ };
+ });
+ });
+}
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_touchScroll.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_touchScroll.js
new file mode 100644
index 0000000000..fa28bf2943
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_touchScroll.js
@@ -0,0 +1,103 @@
+/**
+ * This test is used to ensure that touch in point can activate document and
+ * allow autoplay, but touch scroll can't activate document.
+ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+const PAGE = GetTestWebBasedURL("file_nonAutoplayAudio.html");
+
+function checkMediaPlayingState(isPlaying) {
+ let audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "can't get the audio element!");
+ }
+
+ is(!audio.paused, isPlaying, "media playing state is correct.");
+}
+
+async function callMediaPlay(shouldStartPlaying) {
+ let audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "can't get the audio element!");
+ }
+
+ info(`call media.play().`);
+ let playPromise = new Promise((resolve, reject) => {
+ audio.play().then(() => {
+ audio.isPlayStarted = true;
+ resolve();
+ });
+ content.setTimeout(() => {
+ if (audio.isPlayStarted) {
+ return;
+ }
+ reject();
+ }, 3000);
+ });
+
+ let isStartPlaying = await playPromise.then(
+ () => true,
+ () => false
+ );
+ is(
+ isStartPlaying,
+ shouldStartPlaying,
+ "media is " + (isStartPlaying ? "" : "not ") + "playing."
+ );
+}
+
+async function synthesizeTouchScroll(target, browser) {
+ const offset = 100;
+ await BrowserTestUtils.synthesizeTouch(
+ target,
+ 0,
+ 0,
+ { type: "touchstart" },
+ browser
+ );
+ await BrowserTestUtils.synthesizeTouch(
+ target,
+ offset / 2,
+ offset / 2,
+ { type: "touchmove" },
+ browser
+ );
+ await BrowserTestUtils.synthesizeTouch(
+ target,
+ offset,
+ offset,
+ { type: "touchend" },
+ browser
+ );
+}
+
+add_task(async function setup_test_preference() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ],
+ });
+});
+
+add_task(async function testTouchScroll() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async browser => {
+ info(`- media should not start playing -`);
+ await SpecialPowers.spawn(browser, [false], checkMediaPlayingState);
+
+ info(`- simulate touch scroll which should not activate document -`);
+ await synthesizeTouchScroll("#testAudio", browser);
+ await SpecialPowers.spawn(browser, [false], callMediaPlay);
+
+ info(`- simulate touch at a point which should activate document -`);
+ await BrowserTestUtils.synthesizeTouch("#testAudio", 0, 0, {}, browser);
+ await SpecialPowers.spawn(browser, [true], callMediaPlay);
+ }
+ );
+});
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_user_gestures.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_user_gestures.js
new file mode 100644
index 0000000000..2b9d8c1158
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_user_gestures.js
@@ -0,0 +1,277 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+const VIDEO_PAGE = GetTestWebBasedURL("file_video.html");
+
+const UserGestures = {
+ MOUSE_CLICK: "mouse-click",
+ MOUSE_MOVE: "mouse-move",
+ KEYBOARD_PRESS: "keyboard-press",
+};
+
+const UserGestureTests = [
+ { type: UserGestures.MOUSE_CLICK, isActivationGesture: true },
+ { type: UserGestures.MOUSE_MOVE, isActivationGesture: false },
+ // test different keycode here. printable key, non-printable key and other
+ // special keys.
+ {
+ type: UserGestures.KEYBOARD_PRESS,
+ isActivationGesture: true,
+ keyCode: "a",
+ },
+ {
+ type: UserGestures.KEYBOARD_PRESS,
+ isActivationGesture: false,
+ keyCode: "VK_ESCAPE",
+ },
+ {
+ type: UserGestures.KEYBOARD_PRESS,
+ isActivationGesture: true,
+ keyCode: "VK_RETURN",
+ },
+ {
+ type: UserGestures.KEYBOARD_PRESS,
+ isActivationGesture: true,
+ keyCode: "VK_SPACE",
+ },
+];
+
+/**
+ * This test is used to ensure we would stop blocking autoplay after document
+ * has been activated by user gestures. We would treat mouse clicking, key board
+ * pressing (printable keys or carriage return) as valid user gesture input.
+ */
+add_task(async function startTestUserGestureInput() {
+ info("- setup test preference -");
+ await setupTestPreferences();
+
+ info("- test play when page doesn't be activated -");
+ await testPlayWithoutUserGesture();
+
+ info("- test play after page got user gesture -");
+ for (let idx = 0; idx < UserGestureTests.length; idx++) {
+ info("- test play after page got user gesture -");
+ await testPlayWithUserGesture(UserGestureTests[idx]);
+
+ info("- test web audio with user gesture -");
+ await testWebAudioWithUserGesture(UserGestureTests[idx]);
+ }
+});
+
+/**
+ * testing helper functions
+ */
+function setupTestPreferences() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-event.enabled", true],
+ ["media.autoplay.block-webaudio", true],
+ ],
+ });
+}
+
+function simulateUserGesture(gesture, targetBrowser) {
+ info(`- simulate ${gesture.type} event -`);
+ switch (gesture.type) {
+ case UserGestures.MOUSE_CLICK:
+ return BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ { button: 0 },
+ targetBrowser
+ );
+ case UserGestures.MOUSE_MOVE:
+ return BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ { type: "mousemove" },
+ targetBrowser
+ );
+ case UserGestures.KEYBOARD_PRESS:
+ info(`- keycode=${gesture.keyCode} -`);
+ return BrowserTestUtils.synthesizeKey(gesture.keyCode, {}, targetBrowser);
+ default:
+ ok(false, "undefined user gesture");
+ return false;
+ }
+}
+
+async function testPlayWithoutUserGesture() {
+ info("- open new tab -");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, VIDEO_PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ async function checkAutoplayKeyword() {
+ info("- create an new autoplay video -");
+ let video = content.document.createElement("video");
+ video.src = "gizmo.mp4";
+ video.autoplay = true;
+ let canplayPromise = new Promise(function (resolve) {
+ video.addEventListener(
+ "canplaythrough",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ content.document.body.appendChild(video);
+
+ info("- can't autoplay without user activation -");
+ await canplayPromise;
+ ok(video.paused, "video can't start without user input.");
+ }
+ await SpecialPowers.spawn(tab.linkedBrowser, [], checkAutoplayKeyword);
+
+ async function playVideo() {
+ let video = content.document.getElementById("v");
+ info("- call play() without user activation -");
+ await video.play().catch(function () {
+ ok(video.paused, "video can't start play without user input.");
+ });
+ }
+ await SpecialPowers.spawn(tab.linkedBrowser, [], playVideo);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testPlayWithUserGesture(gesture) {
+ info("- open new tab -");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, VIDEO_PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- simulate user gesture -");
+ await simulateUserGesture(gesture, tab.linkedBrowser);
+
+ info("- call play() -");
+ async function playVideo(gesture) {
+ let video = content.document.getElementById("v");
+ try {
+ await video.play();
+ ok(gesture.isActivationGesture, "user gesture can activate the page");
+ ok(!video.paused, "video starts playing.");
+ } catch (e) {
+ ok(
+ !gesture.isActivationGesture,
+ "user gesture can not activate the page"
+ );
+ ok(video.paused, "video can not start playing.");
+ }
+ }
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [gesture], playVideo);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+}
+
+function createAudioContext() {
+ content.ac = new content.AudioContext();
+ let ac = content.ac;
+ ac.resumePromises = [];
+ ac.stateChangePromise = new Promise(resolve => {
+ ac.addEventListener(
+ "statechange",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ ac.notAllowedToStart = new Promise(resolve => {
+ ac.addEventListener(
+ "blocked",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+function resumeWithoutExpectedSuccess() {
+ let ac = content.ac;
+ let promise = ac.resume();
+ ac.resumePromises.push(promise);
+ return new Promise((resolve, reject) => {
+ content.setTimeout(() => {
+ if (ac.state == "suspended") {
+ ok(true, "audio context is still suspended");
+ resolve();
+ } else {
+ reject("audio context should not be allowed to start");
+ }
+ }, 2000);
+ });
+}
+
+function resumeWithExpectedSuccess() {
+ let ac = content.ac;
+ ac.resumePromises.push(ac.resume());
+ return Promise.all(ac.resumePromises).then(() => {
+ ok(ac.state == "running", "audio context starts running");
+ });
+}
+
+async function testWebAudioWithUserGesture(gesture) {
+ info("- open new tab -");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ info("- create audio context -");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.ac = new content.AudioContext();
+ let ac = content.ac;
+ ac.resumePromises = [];
+ return new Promise(resolve => {
+ ac.addEventListener(
+ "blocked",
+ function () {
+ Assert.equal(
+ ac.state,
+ "suspended",
+ `AudioContext is not started yet.`
+ );
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ });
+
+ info("- calling resume() -");
+ try {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ resumeWithoutExpectedSuccess
+ );
+ } catch (error) {
+ ok(false, error.toString());
+ }
+
+ info("- simulate user gesture -");
+ await simulateUserGesture(gesture, tab.linkedBrowser);
+
+ info("- calling resume() again");
+ try {
+ let resumeFunc = gesture.isActivationGesture
+ ? resumeWithExpectedSuccess
+ : resumeWithoutExpectedSuccess;
+ await SpecialPowers.spawn(tab.linkedBrowser, [], resumeFunc);
+ } catch (error) {
+ ok(false, error.toString());
+ }
+
+ info("- remove tab -");
+ await BrowserTestUtils.removeTab(tab);
+}
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_webRTC_permission.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_webRTC_permission.js
new file mode 100644
index 0000000000..8afae4d08e
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_webRTC_permission.js
@@ -0,0 +1,67 @@
+/**
+ * This test is used to ensure site which has granted 'camera' or 'microphone'
+ * or 'screen' permission could be allowed to autoplay.
+ */
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const VIDEO_PAGE = GetTestWebBasedURL("file_empty.html");
+
+add_task(() => {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-event.enabled", true],
+ ],
+ });
+});
+
+async function testAutoplayWebRTCPermission(args) {
+ info(`- Starting ${args.name} -`);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: VIDEO_PAGE,
+ },
+ async browser => {
+ PermissionTestUtils.add(
+ browser.currentURI,
+ args.permission,
+ Services.perms.ALLOW_ACTION
+ );
+
+ await loadAutoplayVideo(browser, args);
+ await checkVideoDidPlay(browser, args);
+
+ // Reset permission.
+ PermissionTestUtils.remove(browser.currentURI, args.permission);
+
+ info(`- Finished ${args.name} -`);
+ }
+ );
+}
+
+add_task(async function start_test() {
+ await testAutoplayWebRTCPermission({
+ name: "Site with camera permission",
+ permission: "camera",
+ shouldPlay: true,
+ mode: "call play",
+ });
+ await testAutoplayWebRTCPermission({
+ name: "Site with microphone permission",
+ permission: "microphone",
+ shouldPlay: true,
+ mode: "call play",
+ });
+ await testAutoplayWebRTCPermission({
+ name: "Site with screen permission",
+ permission: "screen",
+ shouldPlay: true,
+ mode: "call play",
+ });
+});
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio.js
new file mode 100644
index 0000000000..b6e1133dd1
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio.js
@@ -0,0 +1,217 @@
+/**
+ * This test is used for testing whether WebAudio can be started correctly in
+ * different scenarios, such as
+ * 1) site has existing 'autoplay-media' permission for allowing autoplay
+ * 2) site has existing 'autoplay-media' permission for blocking autoplay
+ * 3) site doesn't have permission, start audio context by calling resume() or
+ * AudioScheduledNode.start() after granting user activation.
+ */
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const PAGE = GetTestWebBasedURL("file_empty.html");
+
+function setup_test_preference() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-webaudio", true],
+ ["media.autoplay.block-event.enabled", true],
+ ],
+ });
+}
+
+function createAudioContext() {
+ content.ac = new content.AudioContext();
+ const ac = content.ac;
+
+ ac.allowedToStart = new Promise(resolve => {
+ ac.addEventListener(
+ "statechange",
+ function () {
+ if (ac.state === "running") {
+ resolve();
+ }
+ },
+ { once: true }
+ );
+ });
+
+ ac.notAllowedToStart = new Promise(resolve => {
+ ac.addEventListener(
+ "blocked",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+async function checkIfAudioContextIsAllowedToStart(isAllowedToStart) {
+ const ac = content.ac;
+ if (isAllowedToStart) {
+ await ac.allowedToStart;
+ ok(ac.state === "running", `AudioContext is running.`);
+ } else {
+ await ac.notAllowedToStart;
+ ok(ac.state === "suspended", `AudioContext is not started yet.`);
+ }
+}
+
+async function resumeAudioContext(isAllowedToStart) {
+ const ac = content.ac;
+ const resumePromise = ac.resume();
+ const blockedPromise = new Promise(resolve => {
+ ac.addEventListener(
+ "blocked",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+
+ if (isAllowedToStart) {
+ await resumePromise;
+ ok(true, `successfully resume AudioContext.`);
+ } else {
+ await blockedPromise;
+ ok(true, `resume is blocked because AudioContext is not allowed to start.`);
+ }
+}
+
+function startAudioContext(method) {
+ const ac = content.ac;
+ if (method == "AudioContext") {
+ info(`using AudioContext.resume() to start AudioContext`);
+ ac.resume();
+ return;
+ }
+ info(`using ${method}.start() to start AudioContext`);
+ let node;
+ switch (method) {
+ case "AudioBufferSourceNode":
+ node = ac.createBufferSource();
+ break;
+ case "ConstantSourceNode":
+ node = ac.createConstantSource();
+ break;
+ case "OscillatorNode":
+ node = ac.createOscillator();
+ break;
+ default:
+ ok(false, "undefined AudioScheduledSourceNode type");
+ return;
+ }
+ node.connect(ac.destination);
+ node.start();
+}
+
+async function testAutoplayExistingPermission({ name, permission }) {
+ info(`- starting \"${name}\" -`);
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ PAGE
+ );
+ const browser = tab.linkedBrowser;
+
+ info(`- set the 'autoplay-media' permission -`);
+ const promptShow = () =>
+ PopupNotifications.getNotification("autoplay-media", browser);
+ PermissionTestUtils.add(browser.currentURI, "autoplay-media", permission);
+ ok(!promptShow(), `should not be showing permission prompt yet`);
+
+ info(`- create audio context -`);
+ await SpecialPowers.spawn(browser, [], createAudioContext);
+
+ info(`- check AudioContext status -`);
+ const isAllowedToStart = permission === Services.perms.ALLOW_ACTION;
+ await SpecialPowers.spawn(
+ browser,
+ [isAllowedToStart],
+ checkIfAudioContextIsAllowedToStart
+ );
+ await SpecialPowers.spawn(browser, [isAllowedToStart], resumeAudioContext);
+
+ info(`- remove tab -`);
+ PermissionTestUtils.remove(browser.currentURI, "autoplay-media");
+ await BrowserTestUtils.removeTab(tab);
+}
+
+async function testAutoplayUnknownPermission({ name, method }) {
+ info(`- starting \"${name}\" -`);
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ PAGE
+ );
+ const browser = tab.linkedBrowser;
+
+ info(`- set the 'autoplay-media' permission to UNKNOWN -`);
+ const promptShow = () =>
+ PopupNotifications.getNotification("autoplay-media", browser);
+ PermissionTestUtils.add(
+ browser.currentURI,
+ "autoplay-media",
+ Services.perms.UNKNOWN_ACTION
+ );
+ ok(!promptShow(), `should not be showing permission prompt yet`);
+
+ info(`- create AudioContext which should not start -`);
+ await SpecialPowers.spawn(browser, [], createAudioContext);
+ await SpecialPowers.spawn(
+ browser,
+ [false],
+ checkIfAudioContextIsAllowedToStart
+ );
+
+ info(`- simulate user activate the page -`);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.notifyUserGestureActivation();
+ });
+
+ info(`- try to start AudioContext -`);
+ await SpecialPowers.spawn(browser, [method], startAudioContext);
+
+ info(`- check AudioContext status -`);
+ await SpecialPowers.spawn(
+ browser,
+ [true],
+ checkIfAudioContextIsAllowedToStart
+ );
+ await SpecialPowers.spawn(browser, [true], resumeAudioContext);
+
+ info(`- remove tab -`);
+ PermissionTestUtils.remove(browser.currentURI, "autoplay-media");
+ await BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function start_tests() {
+ info("- setup test preference -");
+ await setup_test_preference();
+
+ await testAutoplayExistingPermission({
+ name: "Prexisting allow permission",
+ permission: Services.perms.ALLOW_ACTION,
+ });
+ await testAutoplayExistingPermission({
+ name: "Prexisting block permission",
+ permission: Services.perms.DENY_ACTION,
+ });
+ const startMethods = [
+ "AudioContext",
+ "AudioBufferSourceNode",
+ "ConstantSourceNode",
+ "OscillatorNode",
+ ];
+ for (let method of startMethods) {
+ await testAutoplayUnknownPermission({
+ name: "Unknown permission and start AudioContext after granting user activation",
+ method,
+ });
+ }
+});
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio_with_gum.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio_with_gum.js
new file mode 100644
index 0000000000..45d5c71a52
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio_with_gum.js
@@ -0,0 +1,174 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+/**
+ * This test is used to ensure web audio can be allowed to start when we have
+ * GUM permission.
+ */
+add_task(async function startTestingWebAudioWithGUM() {
+ info("- setup test preference -");
+ await setupTestPreferences();
+
+ info("- test web audio with gUM success -");
+ await testWebAudioWithGUM({
+ constraints: { audio: true },
+ shouldAllowStartingContext: true,
+ });
+ await testWebAudioWithGUM({
+ constraints: { video: true },
+ shouldAllowStartingContext: true,
+ });
+ await testWebAudioWithGUM({
+ constraints: { video: true, audio: true },
+ shouldAllowStartingContext: true,
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.navigator.permission.force", true]],
+ }).then(async function () {
+ info("- test web audio with gUM denied -");
+ await testWebAudioWithGUM({
+ constraints: { video: true },
+ shouldAllowStartingContext: false,
+ });
+ await testWebAudioWithGUM({
+ constraints: { audio: true },
+ shouldAllowStartingContext: false,
+ });
+ await testWebAudioWithGUM({
+ constraints: { video: true, audio: true },
+ shouldAllowStartingContext: false,
+ });
+ });
+});
+
+/**
+ * testing helper functions
+ */
+function setupTestPreferences() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-event.enabled", true],
+ ["media.autoplay.block-webaudio", true],
+ ["media.navigator.permission.fake", true],
+ ],
+ });
+}
+
+function createAudioContext() {
+ content.ac = new content.AudioContext();
+ let ac = content.ac;
+ ac.resumePromises = [];
+ ac.stateChangePromise = new Promise(resolve => {
+ ac.addEventListener(
+ "statechange",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ ac.notAllowedToStart = new Promise(resolve => {
+ ac.addEventListener(
+ "blocked",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+async function checkingAudioContextRunningState() {
+ let ac = content.ac;
+ await ac.notAllowedToStart;
+ ok(ac.state === "suspended", `AudioContext is not started yet.`);
+}
+
+function resumeWithoutExpectedSuccess() {
+ let ac = content.ac;
+ let promise = ac.resume();
+ ac.resumePromises.push(promise);
+ return new Promise((resolve, reject) => {
+ content.setTimeout(() => {
+ if (ac.state == "suspended") {
+ ok(true, "audio context is still suspended");
+ resolve();
+ } else {
+ reject("audio context should not be allowed to start");
+ }
+ }, 2000);
+ });
+}
+
+function resumeWithExpectedSuccess() {
+ let ac = content.ac;
+ ac.resumePromises.push(ac.resume());
+ return Promise.all(ac.resumePromises).then(() => {
+ ok(ac.state == "running", "audio context starts running");
+ });
+}
+
+async function callGUM(testParameters) {
+ info("- calling gum with " + JSON.stringify(testParameters.constraints));
+ if (testParameters.shouldAllowStartingContext) {
+ // Because of the prefs we've set and passed, this is going to allow the
+ // window to start an AudioContext synchronously.
+ testParameters.constraints.fake = true;
+ await content.navigator.mediaDevices.getUserMedia(
+ testParameters.constraints
+ );
+ return;
+ }
+
+ // Call gUM, without sucess: we've made it so that only fake requests
+ // succeed without permission, and this is requesting non-fake-devices. Return
+ // a resolved promise so that the test continues, but the getUserMedia Promise
+ // will never be resolved.
+ // We do this to check that it's not merely calling gUM that allows starting
+ // an AudioContext, it's having the Promise it return resolved successfuly,
+ // because of saved permissions for an origin or explicit user consent using
+ // the prompt.
+ content.navigator.mediaDevices.getUserMedia(testParameters.constraints);
+}
+
+async function testWebAudioWithGUM(testParameters) {
+ info("- open new tab -");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "https://example.com"
+ );
+ info("- create audio context -");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], createAudioContext);
+
+ info("- check whether audio context starts running -");
+ try {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ checkingAudioContextRunningState
+ );
+ } catch (error) {
+ ok(false, error.toString());
+ }
+
+ try {
+ await SpecialPowers.spawn(tab.linkedBrowser, [testParameters], callGUM);
+ } catch (error) {
+ ok(false, error.toString());
+ }
+
+ info("- calling resume() again");
+ try {
+ let resumeFunc = testParameters.shouldAllowStartingContext
+ ? resumeWithExpectedSuccess
+ : resumeWithoutExpectedSuccess;
+ await SpecialPowers.spawn(tab.linkedBrowser, [], resumeFunc);
+ } catch (error) {
+ ok(false, error.toString());
+ }
+
+ info("- remove tab -");
+ await BrowserTestUtils.removeTab(tab);
+}
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_videoDocument.js b/dom/media/autoplay/test/browser/browser_autoplay_videoDocument.js
new file mode 100644
index 0000000000..77ce4ddbc1
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_videoDocument.js
@@ -0,0 +1,80 @@
+"use strict";
+
+const PAGE = GetTestWebBasedURL("audio.ogg");
+
+function setup_test_preference() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ],
+ });
+}
+
+async function checkIsVideoDocumentAutoplay(browser) {
+ const played = await SpecialPowers.spawn(browser, [], async () => {
+ const video = content.document.getElementsByTagName("video")[0];
+ const played =
+ video &&
+ (await video.play().then(
+ () => true,
+ () => false
+ ));
+ return played;
+ });
+ ok(played, "Should be able to play in video document.");
+}
+
+async function checkIsIframeVideoDocumentAutoplay(browser) {
+ info("- create iframe video document -");
+ const iframeBC = await SpecialPowers.spawn(browser, [PAGE], async pageURL => {
+ const iframe = content.document.createElement("iframe");
+ iframe.src = pageURL;
+ content.document.body.appendChild(iframe);
+ const iframeLoaded = new Promise((resolve, reject) => {
+ iframe.addEventListener("load", e => resolve(), { once: true });
+ });
+ await iframeLoaded;
+ return iframe.browsingContext;
+ });
+
+ info("- check whether iframe video document starts playing -");
+ const [paused, playedLength] = await SpecialPowers.spawn(iframeBC, [], () => {
+ const video = content.document.querySelector("video");
+ return [video.paused, video.played.length];
+ });
+ ok(paused, "Subdoc video should not have played");
+ is(playedLength, 0, "Should have empty played ranges");
+}
+
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async browser => {
+ info("- setup test preference -");
+ await setup_test_preference();
+
+ info(`- check whether video document is autoplay -`);
+ await checkIsVideoDocumentAutoplay(browser);
+ }
+ );
+});
+
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ async browser => {
+ info("- setup test preference -");
+ await setup_test_preference();
+
+ info(`- check whether video document in iframe is autoplay -`);
+ await checkIsIframeVideoDocumentAutoplay(browser);
+ }
+ );
+});
diff --git a/dom/media/autoplay/test/browser/file_empty.html b/dom/media/autoplay/test/browser/file_empty.html
new file mode 100644
index 0000000000..d2b0361f09
--- /dev/null
+++ b/dom/media/autoplay/test/browser/file_empty.html
@@ -0,0 +1,8 @@
+<!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..c9982f932a
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html
@@ -0,0 +1,148 @@
+<!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",
+ "OS",
+ "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.ini b/dom/media/autoplay/test/mochitest/mochitest.ini
new file mode 100644
index 0000000000..f25b12e953
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/mochitest.ini
@@ -0,0 +1,54 @@
+[DEFAULT]
+subsuite = media
+tags = autoplay
+support-files =
+ ../../../test/manifest.js
+ ../../../test/320x240.ogv
+ ../../../test/bogus.duh
+ ../../../test/detodos-short.opus
+ ../../../test/flac-s24.flac
+ ../../../test/gizmo.mp4
+ ../../../test/gizmo.webm
+ ../../../test/gizmo-noaudio.mp4
+ ../../../test/gizmo-noaudio.webm
+ ../../../test/gizmo-short.mp4
+ ../../../test/r11025_s16_c1-short.wav
+ ../../../test/sample.3g2
+ ../../../test/sample.3gp
+ ../../../test/short.mp4
+ ../../../test/seek-short.webm
+ ../../../test/small-shot.flac
+ ../../../test/small-shot.m4a
+ ../../../test/small-shot.mp3
+ ../../../test/small-shot-mp3.mp4
+ ../../../test/small-shot.ogg
+ ../../../test/vp9-short.webm
+ AutoplayTestUtils.js
+ file_autoplay_gv_play_request_frame.html
+ file_autoplay_gv_play_request_window.html
+ file_autoplay_policy_activation_frame.html
+ file_autoplay_policy_activation_window.html
+ file_autoplay_policy_eventdown_activation.html
+ file_autoplay_policy_play_before_loadedmetadata.html
+ file_autoplay_policy_unmute_pauses.html
+ file_autoplay_policy_key_blacklist.html
+
+[test_autoplay.html]
+[test_autoplay_contentEditable.html]
+[test_autoplay_gv_play_request.html]
+skip-if = toolkit != 'android'
+[test_autoplay_policy.html]
+[test_autoplay_policy_activation.html]
+[test_autoplay_policy_play_before_loadedmetadata.html]
+skip-if = toolkit == 'android' # bug 1591121
+[test_autoplay_policy_eventdown_activation.html]
+[test_autoplay_policy_permission.html]
+[test_autoplay_policy_unmute_pauses.html]
+[test_autoplay_policy_key_blacklist.html]
+skip-if = (verify && debug && (os == 'win')) # bug 1424903
+[test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html]
+[test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html]
+[test_autoplay_policy_web_audio_AudioParamStream.html]
+[test_autoplay_policy_web_audio_createMediaStreamSource.html]
+[test_streams_autoplay.html]
+tags=mtg capturestream
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay.html b/dom/media/autoplay/test/mochitest/test_autoplay.html
new file mode 100644
index 0000000000..aa936f976d
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay.html
@@ -0,0 +1,36 @@
+<!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..27dfa5388f
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_AudioParamStream.html
@@ -0,0 +1,171 @@
+<!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-webaudio", true],
+ ["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..5fe9aa64fc
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_createMediaStreamSource.html
@@ -0,0 +1,119 @@
+<!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-webaudio", true],
+ ["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..41fab54133
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html
@@ -0,0 +1,105 @@
+<!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-webaudio", true],
+ ["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..46df256391
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html
@@ -0,0 +1,96 @@
+<!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-webaudio", true],
+ ["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>