merge mozilla-inbound to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 28 Jun 2016 16:09:05 +0200
changeset 302885 e45890951ce77c3df05575bd54072b9f300d77b0
parent 302884 cf243ad179e9929358583f4b8e6f8ce709f85eb6 (current diff)
parent 302843 581d221488b88275575d6b11dc1a999918deb6bd (diff)
child 302886 e5bc108c46ea9b00bb639e73c857ecc6ddf05379
child 303039 910bb13e5929010e0e34b6a08fd3bcf6f5657b6e
child 303072 e774866bf8a12537e451ccc4b38ffbf0610b5d49
push id78900
push usercbook@mozilla.com
push dateTue, 28 Jun 2016 14:14:30 +0000
treeherdermozilla-inbound@e5bc108c46ea [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone50.0a1
first release with
nightly linux32
e45890951ce7 / 50.0a1 / 20160629030209 / files
nightly linux64
e45890951ce7 / 50.0a1 / 20160629030209 / files
nightly mac
e45890951ce7 / 50.0a1 / 20160629030209 / files
nightly win32
e45890951ce7 / 50.0a1 / 20160629030209 / files
nightly win64
e45890951ce7 / 50.0a1 / 20160629030209 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge mozilla-inbound to mozilla-central a=merge
dom/base/nsIXMLHttpRequest.idl
dom/base/nsXMLHttpRequest.cpp
dom/base/nsXMLHttpRequest.h
dom/base/test/echo.sjs
dom/base/test/file_XHRDocURI.html
dom/base/test/file_XHRDocURI.html^headers^
dom/base/test/file_XHRDocURI.sjs
dom/base/test/file_XHRDocURI.text
dom/base/test/file_XHRDocURI.text^headers^
dom/base/test/file_XHRDocURI.xml
dom/base/test/file_XHRDocURI.xml^headers^
dom/base/test/file_XHRResponseURL.js
dom/base/test/file_XHRResponseURL.sjs
dom/base/test/file_XHRResponseURL.text
dom/base/test/file_XHRResponseURL.text^headers^
dom/base/test/file_XHRResponseURL_nocors.text
dom/base/test/file_XHRSendData.sjs
dom/base/test/file_XHRSendData_doc.xml
dom/base/test/file_XHRSendData_doc.xml^headers^
dom/base/test/file_XHR_anon.sjs
dom/base/test/file_XHR_binary1.bin
dom/base/test/file_XHR_binary1.bin^headers^
dom/base/test/file_XHR_binary2.bin
dom/base/test/file_XHR_fail1.txt
dom/base/test/file_XHR_fail1.txt^headers^
dom/base/test/file_XHR_fail1b.txt
dom/base/test/file_XHR_header.sjs
dom/base/test/file_XHR_pass1.xml
dom/base/test/file_XHR_pass2.txt
dom/base/test/file_XHR_pass3.txt
dom/base/test/file_XHR_pass3.txt^headers^
dom/base/test/file_XHR_system_redirect.html
dom/base/test/file_XHR_system_redirect.html^headers^
dom/base/test/file_XHR_timeout.sjs
dom/base/test/file_html_in_xhr.html
dom/base/test/file_html_in_xhr.sjs
dom/base/test/file_html_in_xhr2.html
dom/base/test/file_html_in_xhr3.html
dom/base/test/progressserver.sjs
dom/base/test/responseIdentical.sjs
dom/base/test/test_XHR.html
dom/base/test/test_XHRDocURI.html
dom/base/test/test_XHRResponseURL.html
dom/base/test/test_XHRSendData.html
dom/base/test/test_XHR_anon.html
dom/base/test/test_XHR_header.html
dom/base/test/test_XHR_onuploadprogress.html
dom/base/test/test_XHR_parameters.html
dom/base/test/test_XHR_system.html
dom/base/test/test_XHR_timeout.html
dom/base/test/test_XHR_timeout.js
dom/base/test/test_html_in_xhr.html
dom/base/test/test_sync_xhr_timer.xhtml
dom/base/test/test_xhr_abort_after_load.html
dom/base/test/test_xhr_forbidden_headers.html
dom/base/test/test_xhr_overridemimetype_throws_on_invalid_state.html
dom/base/test/test_xhr_progressevents.html
dom/base/test/test_xhr_send.html
dom/base/test/test_xhr_send_readystate.html
dom/base/test/test_xhr_withCredentials.html
dom/gamepad/GamepadFunctions.cpp
dom/gamepad/GamepadFunctions.h
dom/interfaces/gamepad/moz.build
dom/interfaces/gamepad/nsIGamepadServiceTest.idl
dom/tests/mochitest/gamepad/gamepad_service_test_chrome_script.js
dom/workers/XMLHttpRequest.cpp
dom/workers/XMLHttpRequest.h
dom/workers/XMLHttpRequestUpload.cpp
dom/workers/XMLHttpRequestUpload.h
dom/workers/test/file_getcookie.sjs
dom/workers/test/relativeLoad_import.js
dom/workers/test/relativeLoad_worker.js
dom/workers/test/relativeLoad_worker2.js
dom/workers/test/subdir/relativeLoad_sub_import.js
dom/workers/test/subdir/relativeLoad_sub_worker.js
dom/workers/test/subdir/relativeLoad_sub_worker2.js
dom/workers/test/terminateSyncXHR_frame.html
dom/workers/test/terminateSyncXHR_worker.js
dom/workers/test/testXHR.txt
dom/workers/test/test_relativeLoad.html
dom/workers/test/test_terminateSyncXHR.html
dom/workers/test/test_xhr.html
dom/workers/test/test_xhr2.html
dom/workers/test/test_xhrAbort.html
dom/workers/test/test_xhr_3rdparty.html
dom/workers/test/test_xhr_cors_redirect.html
dom/workers/test/test_xhr_headers.html
dom/workers/test/test_xhr_implicit_cancel.html
dom/workers/test/test_xhr_parameters.html
dom/workers/test/test_xhr_parameters.js
dom/workers/test/test_xhr_responseURL.html
dom/workers/test/test_xhr_system.html
dom/workers/test/test_xhr_system.js
dom/workers/test/test_xhr_timeout.html
dom/workers/test/xhr2_worker.js
dom/workers/test/xhrAbort_worker.js
dom/workers/test/xhr_cors_redirect.js
dom/workers/test/xhr_cors_redirect.sjs
dom/workers/test/xhr_headers_server.sjs
dom/workers/test/xhr_headers_worker.js
dom/workers/test/xhr_implicit_cancel_worker.js
dom/workers/test/xhr_worker.js
gfx/angle/include/KHR/khrplatform.h
gfx/angle/src/compiler/preprocessor/generate_parser.sh
gfx/angle/src/libANGLE/Data.cpp
gfx/angle/src/libANGLE/Data.h
gfx/angle/src/libANGLE/renderer/ImplFactory.h
gfx/angle/src/libANGLE/renderer/Renderer.cpp
gfx/angle/src/libANGLE/renderer/Renderer.h
gfx/angle/src/libANGLE/renderer/d3d/copyimage.cpp
gfx/angle/src/libANGLE/renderer/d3d/copyimage.h
gfx/angle/src/libANGLE/renderer/d3d/copyimage.inl
gfx/angle/src/libANGLE/renderer/d3d/d3d11/NativeWindow.h
gfx/angle/src/libANGLE/renderer/d3d/d3d11/gen_swizzle_format_table.py
gfx/angle/src/libANGLE/renderer/d3d/d3d11/internal_format_initializer_table.cpp
gfx/angle/src/libANGLE/renderer/d3d/d3d11/internal_format_initializer_table.h
gfx/angle/src/libANGLE/renderer/d3d/d3d11/swizzle_format_data.json
gfx/angle/src/libANGLE/renderer/d3d/d3d11/swizzle_format_info.h
gfx/angle/src/libANGLE/renderer/d3d/d3d11/swizzle_format_info_autogen.cpp
gfx/angle/src/libANGLE/renderer/d3d/d3d11/win32/NativeWindow.cpp
gfx/angle/src/libANGLE/renderer/d3d/formatutilsD3D.cpp
gfx/angle/src/libANGLE/renderer/d3d/imageformats.h
gfx/angle/src/libANGLE/renderer/generate_new_renderer.py
gfx/angle/src/tests/gl_tests/FramebufferFormatsTest.cpp
js/src/tests/ecma/jsref.js
js/src/tests/ecma_2/jsref.js
js/src/tests/js1_1/jsref.js
js/src/tests/js1_2/jsref.js
js/src/tests/js1_3/jsref.js
js/src/tests/js1_4/jsref.js
js/src/tests/js1_5/Regress/regress-416628.js
modules/libpref/init/all.js
modules/woff2/missing-assert-header.patch
taskcluster/taskgraph/types.py
testing/docker/desktop-test/bin/run-wizard
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -208,16 +208,17 @@
 @RESPATH@/components/dom_tv.xpt
 @RESPATH@/components/dom_inputport.xpt
 @RESPATH@/components/dom_views.xpt
 @RESPATH@/components/dom_voicemail.xpt
 #ifdef MOZ_WEBSPEECH
 @RESPATH@/components/dom_webspeechrecognition.xpt
 #endif
 @RESPATH@/components/dom_xbl.xpt
+@RESPATH@/components/dom_xhr.xpt
 @RESPATH@/components/dom_xpath.xpt
 @RESPATH@/components/dom_xul.xpt
 @RESPATH@/components/dom_time.xpt
 @RESPATH@/components/dom_presentation.xpt
 @RESPATH@/components/downloads.xpt
 @RESPATH@/components/editor.xpt
 @RESPATH@/components/embed_base.xpt
 @RESPATH@/components/extensions.xpt
--- a/browser/base/content/contentSearchUI.js
+++ b/browser/base/content/contentSearchUI.js
@@ -77,16 +77,19 @@ ContentSearchUIController.prototype = {
   // if it's set when the suggestions table is actually opened.
   _pendingOneOffRefresh: undefined,
 
   get defaultEngine() {
     return this._defaultEngine;
   },
 
   set defaultEngine(engine) {
+    if (this._defaultEngine && this._defaultEngine.icon) {
+      URL.revokeObjectURL(this._defaultEngine.icon);
+    }
     let icon;
     if (engine.iconBuffer) {
       icon = this._getFaviconURIFromBuffer(engine.iconBuffer);
     }
     else {
       icon = this._getImageURIForCurrentResolution(
         "chrome://mozapps/skin/places/defaultFavicon.png");
     }
@@ -863,16 +866,20 @@ ContentSearchUIController.prototype = {
       if (engine.iconBuffer) {
         uri = this._getFaviconURIFromBuffer(engine.iconBuffer);
       }
       else {
         uri = this._getImageURIForCurrentResolution(
           "chrome://browser/skin/search-engine-placeholder.png");
       }
       img.setAttribute("src", uri);
+      img.addEventListener("load", function imgLoad() {
+        img.removeEventListener("load", imgLoad);
+        URL.revokeObjectURL(uri);
+      });
       button.appendChild(img);
       button.style.width = buttonWidth + "px";
       button.setAttribute("title", engine.name);
 
       button.engineName = engine.name;
       button.addEventListener("click", this);
       button.addEventListener("mousemove", this);
 
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -211,21 +211,19 @@
 @RESPATH@/components/dom_traversal.xpt
 @RESPATH@/components/dom_tv.xpt
 @RESPATH@/components/dom_voicemail.xpt
 #ifdef MOZ_WEBSPEECH
 @RESPATH@/components/dom_webspeechrecognition.xpt
 #endif
 @RESPATH@/components/dom_workers.xpt
 @RESPATH@/components/dom_xbl.xpt
+@RESPATH@/components/dom_xhr.xpt
 @RESPATH@/components/dom_xpath.xpt
 @RESPATH@/components/dom_xul.xpt
-#ifdef MOZ_GAMEPAD
-@RESPATH@/components/dom_gamepad.xpt
-#endif
 @RESPATH@/components/dom_payment.xpt
 @RESPATH@/components/dom_presentation.xpt
 @RESPATH@/components/downloads.xpt
 @RESPATH@/components/editor.xpt
 @RESPATH@/components/embed_base.xpt
 @RESPATH@/components/extensions.xpt
 @RESPATH@/components/exthandler.xpt
 @RESPATH@/components/exthelper.xpt
--- a/build/annotationProcessors/CodeGenerator.java
+++ b/build/annotationProcessors/CodeGenerator.java
@@ -37,23 +37,24 @@ public class CodeGenerator {
         this.clsName = annotatedClass.generatedName;
 
         final String unqualifiedName = Utils.getUnqualifiedName(clsName);
         header.append(
                 "class " + clsName + " : public mozilla::jni::ObjectBase<" +
                         unqualifiedName + ", jobject>\n" +
                 "{\n" +
                 "public:\n" +
+                "    static const char name[];\n" +
+                "\n" +
                 "    explicit " + unqualifiedName + "(const Context& ctx) : ObjectBase<" +
                         unqualifiedName + ", jobject>(ctx) {}\n" +
                 "\n");
 
         cpp.append(
-                "template<> const char mozilla::jni::Context<" +
-                        clsName + ", jobject>::name[] =\n" +
+                "const char " + clsName + "::name[] =\n" +
                 "        \"" + cls.getName().replace('.', '/') + "\";\n" +
                 "\n");
 
         natives.append(
                 "template<class Impl>\n" +
                 "class " + clsName + "::Natives : " +
                         "public mozilla::jni::NativeImpl<" + unqualifiedName + ", Impl>\n" +
                 "{\n" +
--- a/devtools/client/locales/en-US/markers.properties
+++ b/devtools/client/locales/en-US/markers.properties
@@ -98,17 +98,16 @@ marker.value.DOMEventTargetPhase=Target
 marker.value.DOMEventCapturingPhase=Capture
 marker.value.DOMEventBubblingPhase=Bubbling
 
 # LOCALIZATION NOTE (marker.gcreason.label.*):
 # These strings are used to give a concise but readable description of a GC reason.
 marker.gcreason.label.API=API Call
 marker.gcreason.label.EAGER_ALLOC_TRIGGER=Eager Allocation Trigger
 marker.gcreason.label.DESTROY_RUNTIME=Shutdown
-marker.gcreason.label.DESTROY_CONTEXT=Shutdown
 marker.gcreason.label.LAST_DITCH=Out of Memory
 marker.gcreason.label.TOO_MUCH_MALLOC=Too Many Bytes Allocated
 marker.gcreason.label.ALLOC_TRIGGER=Too Many Allocations
 marker.gcreason.label.DEBUG_GC=Debug GC
 marker.gcreason.label.COMPARTMENT_REVIVED=Dead Global Revived
 marker.gcreason.label.RESET=Finish Incremental Cycle
 marker.gcreason.label.OUT_OF_NURSERY=Nursery is Full
 marker.gcreason.label.EVICT_NURSERY=Nursery Eviction
@@ -138,17 +137,16 @@ marker.gcreason.label.USER_INACTIVE=User
 # The name of a nursery collection.
 marker.nurseryCollection=Nursery Collection
 
 # LOCALIZATION NOTE (marker.gcreason.description.*):
 # These strings are used to give an expanded description of why a GC occurred.
 marker.gcreason.description.API=There was an API call to force garbage collection.
 marker.gcreason.description.EAGER_ALLOC_TRIGGER=JavaScript returned to the event loop and there were enough bytes allocated since the last GC that a new GC cycle was triggered.
 marker.gcreason.description.DESTROY_RUNTIME=Firefox destroyed a JavaScript runtime or context, and this was the final garbage collection before shutting down.
-marker.gcreason.description.DESTROY_CONTEXT=Firefox destroyed a JavaScript runtime or context, and this was the final garbage collection before shutting down.
 marker.gcreason.description.LAST_DITCH=JavaScript attempted to allocate, but there was no memory available. Doing a full compacting garbage collection as an attempt to free up memory for the allocation.
 marker.gcreason.description.TOO_MUCH_MALLOC=JavaScript allocated too many bytes, and forced a garbage collection.
 marker.gcreason.description.ALLOC_TRIGGER=JavaScript allocated too many times, and forced a garbage collection.
 marker.gcreason.description.DEBUG_GC=GC due to Zeal debug settings.
 marker.gcreason.description.COMPARTMENT_REVIVED=A global object that was thought to be dead at the start of the GC cycle was revived by the end of the GC cycle.
 marker.gcreason.description.RESET=The active incremental GC cycle was forced to finish immediately.
 marker.gcreason.description.OUT_OF_NURSERY=JavaScript allocated enough new objects in the nursery that it became full and triggered a minor GC.
 marker.gcreason.description.EVICT_NURSERY=Work needed to be done on the tenured heap, requiring the nursery to be empty.
--- a/dom/animation/KeyframeEffect.cpp
+++ b/dom/animation/KeyframeEffect.cpp
@@ -10,21 +10,23 @@
 #include "mozilla/dom/KeyframeEffectBinding.h"
 #include "mozilla/AnimationUtils.h"
 #include "mozilla/FloatingPoint.h"
 #include "mozilla/LookAndFeel.h" // For LookAndFeel::GetInt
 #include "mozilla/KeyframeUtils.h"
 #include "mozilla/StyleAnimationValue.h"
 #include "Layers.h" // For Layer
 #include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetStyleContextForElement
+#include "nsContentUtils.h"  // nsContentUtils::ReportToConsole
 #include "nsCSSPropertySet.h"
 #include "nsCSSProps.h" // For nsCSSProps::PropHasFlags
 #include "nsCSSPseudoElements.h" // For CSSPseudoElementType
 #include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch
 #include "nsIPresShell.h" // For nsIPresShell
+#include "nsIScriptError.h"
 
 namespace mozilla {
 
 // Helper functions for generating a ComputedTimingProperties dictionary
 static void
 GetComputedTimingDictionary(const ComputedTiming& aComputedTiming,
                             const Nullable<TimeDuration>& aLocalTime,
                             const TimingParams& aTiming,
@@ -72,30 +74,34 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_
 NS_INTERFACE_MAP_END_INHERITING(AnimationEffectReadOnly)
 
 NS_IMPL_ADDREF_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly)
 NS_IMPL_RELEASE_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly)
 
 KeyframeEffectReadOnly::KeyframeEffectReadOnly(
   nsIDocument* aDocument,
   const Maybe<OwningAnimationTarget>& aTarget,
-  const TimingParams& aTiming)
+  const TimingParams& aTiming,
+  const KeyframeEffectParams& aOptions)
   : KeyframeEffectReadOnly(aDocument, aTarget,
                            new AnimationEffectTimingReadOnly(aDocument,
-                                                             aTiming))
+                                                             aTiming),
+                           aOptions)
 {
 }
 
 KeyframeEffectReadOnly::KeyframeEffectReadOnly(
   nsIDocument* aDocument,
   const Maybe<OwningAnimationTarget>& aTarget,
-  AnimationEffectTimingReadOnly* aTiming)
+  AnimationEffectTimingReadOnly* aTiming,
+  const KeyframeEffectParams& aOptions)
   : AnimationEffectReadOnly(aDocument)
   , mTarget(aTarget)
   , mTiming(aTiming)
+  , mEffectOptions(aOptions)
   , mInEffectOnLastAnimationTimingUpdate(false)
   , mCumulativeChangeHint(nsChangeHint(0))
 {
   MOZ_ASSERT(aTiming);
 }
 
 JSObject*
 KeyframeEffectReadOnly::WrapObject(JSContext* aCx,
@@ -437,39 +443,33 @@ KeyframeEffectReadOnly::SetKeyframes(JSC
   }
 
   nsTArray<Keyframe> keyframes =
     KeyframeUtils::GetKeyframesFromObject(aContext, aKeyframes, aRv);
   if (aRv.Failed()) {
     return;
   }
 
-  RefPtr<nsStyleContext> styleContext;
-  nsIPresShell* shell = doc->GetShell();
-  if (shell && mTarget) {
-    nsIAtom* pseudo =
-      mTarget->mPseudoType < CSSPseudoElementType::Count ?
-      nsCSSPseudoElements::GetPseudoAtom(mTarget->mPseudoType) : nullptr;
-    styleContext =
-      nsComputedDOMStyle::GetStyleContextForElement(mTarget->mElement,
-                                                    pseudo, shell);
-  }
-
+  RefPtr<nsStyleContext> styleContext = GetTargetStyleContext(doc);
   SetKeyframes(Move(keyframes), styleContext);
 }
 
 void
 KeyframeEffectReadOnly::SetKeyframes(nsTArray<Keyframe>&& aKeyframes,
                                   nsStyleContext* aStyleContext)
 {
   if (KeyframesEqualIgnoringComputedOffsets(aKeyframes, mKeyframes)) {
     return;
   }
 
   mKeyframes = Move(aKeyframes);
+  // Apply distribute spacing irrespective of the spacing mode. We will apply
+  // the specified spacing mode when we generate computed animation property
+  // values from the keyframes since both operations require a style context
+  // and need to be performed whenever the style context changes.
   KeyframeUtils::ApplyDistributeSpacing(mKeyframes);
 
   if (mAnimation && mAnimation->IsRelevant()) {
     nsNodeUtils::AnimationChanged(mAnimation);
   }
 
   if (aStyleContext) {
     UpdateProperties(aStyleContext);
@@ -507,21 +507,30 @@ KeyframeEffectReadOnly::HasAnimationOfPr
 
 void
 KeyframeEffectReadOnly::UpdateProperties(nsStyleContext* aStyleContext)
 {
   MOZ_ASSERT(aStyleContext);
 
   nsTArray<AnimationProperty> properties;
   if (mTarget) {
+    nsTArray<ComputedKeyframeValues> computedValues =
+      KeyframeUtils::GetComputedKeyframeValues(mKeyframes, mTarget->mElement,
+                                               aStyleContext);
+
+    if (mEffectOptions.mSpacingMode == SpacingMode::paced) {
+      KeyframeUtils::ApplySpacing(mKeyframes, SpacingMode::paced,
+                                  mEffectOptions.mPacedProperty,
+                                  computedValues);
+    }
+
     properties =
-      KeyframeUtils::GetAnimationPropertiesFromKeyframes(aStyleContext,
-                                                         mTarget->mElement,
-                                                         mTarget->mPseudoType,
-                                                         mKeyframes);
+      KeyframeUtils::GetAnimationPropertiesFromKeyframes(mKeyframes,
+                                                         computedValues,
+                                                         aStyleContext);
   }
 
   if (mProperties == properties) {
     return;
   }
 
   // Preserve the state of mWinsInCascade and mIsRunningOnCompositor flags.
   nsCSSPropertySet winningInCascadeProperties;
@@ -701,16 +710,51 @@ KeyframeEffectReadOnly::ResetIsRunningOn
     property.mIsRunningOnCompositor = false;
   }
 }
 
 KeyframeEffectReadOnly::~KeyframeEffectReadOnly()
 {
 }
 
+static const KeyframeEffectOptions&
+KeyframeEffectOptionsFromUnion(
+  const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions)
+{
+  MOZ_ASSERT(aOptions.IsKeyframeEffectOptions());
+  return aOptions.GetAsKeyframeEffectOptions();
+}
+
+static const KeyframeEffectOptions&
+KeyframeEffectOptionsFromUnion(
+  const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions)
+{
+  MOZ_ASSERT(aOptions.IsKeyframeAnimationOptions());
+  return aOptions.GetAsKeyframeAnimationOptions();
+}
+
+template <class OptionsType>
+static KeyframeEffectParams
+KeyframeEffectParamsFromUnion(const OptionsType& aOptions,
+                              nsAString& aInvalidPacedProperty,
+                              ErrorResult& aRv)
+{
+  KeyframeEffectParams result;
+  if (!aOptions.IsUnrestrictedDouble()) {
+    const KeyframeEffectOptions& options =
+      KeyframeEffectOptionsFromUnion(aOptions);
+    KeyframeEffectParams::ParseSpacing(options.mSpacing,
+                                       result.mSpacingMode,
+                                       result.mPacedProperty,
+                                       aInvalidPacedProperty,
+                                       aRv);
+  }
+  return result;
+}
+
 static Maybe<OwningAnimationTarget>
 ConvertTarget(const Nullable<ElementOrCSSPseudoElement>& aTarget)
 {
   // Return value optimization.
   Maybe<OwningAnimationTarget> result;
 
   if (aTarget.IsNull()) {
     return result;
@@ -745,19 +789,36 @@ KeyframeEffectReadOnly::ConstructKeyfram
   }
 
   TimingParams timingParams =
     TimingParams::FromOptionsUnion(aOptions, doc, aRv);
   if (aRv.Failed()) {
     return nullptr;
   }
 
+  nsAutoString invalidPacedProperty;
+  KeyframeEffectParams effectOptions =
+    KeyframeEffectParamsFromUnion(aOptions, invalidPacedProperty, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  if (!invalidPacedProperty.IsEmpty()) {
+    const char16_t* params[] = { invalidPacedProperty.get() };
+    nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+                                    NS_LITERAL_CSTRING("Animation"),
+                                    doc,
+                                    nsContentUtils::eDOM_PROPERTIES,
+                                    "UnanimatablePacedProperty",
+                                    params, ArrayLength(params));
+  }
+
   Maybe<OwningAnimationTarget> target = ConvertTarget(aTarget);
   RefPtr<KeyframeEffectType> effect =
-    new KeyframeEffectType(doc, target, timingParams);
+    new KeyframeEffectType(doc, target, timingParams, effectOptions);
 
   effect->SetKeyframes(aGlobal.Context(), aKeyframes, aRv);
   if (aRv.Failed()) {
     return nullptr;
   }
 
   return effect.forget();
 }
@@ -815,16 +876,38 @@ KeyframeEffectReadOnly::RequestRestyle(
   nsPresContext* presContext = GetPresContext();
   if (presContext && mTarget && mAnimation) {
     presContext->EffectCompositor()->
       RequestRestyle(mTarget->mElement, mTarget->mPseudoType,
                      aRestyleType, mAnimation->CascadeLevel());
   }
 }
 
+already_AddRefed<nsStyleContext>
+KeyframeEffectReadOnly::GetTargetStyleContext(nsIDocument* aDoc)
+{
+  if (!mTarget) {
+    return nullptr;
+  }
+
+  if (!aDoc) {
+    aDoc = mTarget->mElement->OwnerDoc();
+    if (!aDoc) {
+      return nullptr;
+    }
+  }
+
+  nsIAtom* pseudo = mTarget->mPseudoType < CSSPseudoElementType::Count
+                    ? nsCSSPseudoElements::GetPseudoAtom(mTarget->mPseudoType)
+                    : nullptr;
+  return nsComputedDOMStyle::GetStyleContextForElement(mTarget->mElement,
+                                                       pseudo,
+                                                       aDoc->GetShell());
+}
+
 #ifdef DEBUG
 void
 DumpAnimationProperties(nsTArray<AnimationProperty>& aAnimationProperties)
 {
   for (auto& p : aAnimationProperties) {
     printf("%s\n", nsCSSProps::GetStringValue(p.mProperty).get());
     for (auto& s : p.mSegments) {
       nsString fromValue, toValue;
@@ -983,16 +1066,18 @@ KeyframeEffectReadOnly::GetKeyframes(JSC
   }
 
   for (const Keyframe& keyframe : mKeyframes) {
     // Set up a dictionary object for the explicit members
     BaseComputedKeyframe keyframeDict;
     if (keyframe.mOffset) {
       keyframeDict.mOffset.SetValue(keyframe.mOffset.value());
     }
+    MOZ_ASSERT(keyframe.mComputedOffset != Keyframe::kComputedOffsetNotSet,
+               "Invalid computed offset");
     keyframeDict.mComputedOffset.Construct(keyframe.mComputedOffset);
     if (keyframe.mTimingFunction) {
       keyframeDict.mEasing.Truncate();
       keyframe.mTimingFunction.ref().AppendToString(keyframeDict.mEasing);
     } // else if null, leave easing as its default "linear".
 
     JS::Rooted<JS::Value> keyframeJSValue(aCx);
     if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) {
@@ -1362,19 +1447,21 @@ KeyframeEffectReadOnly::CanIgnoreIfNotVi
 //---------------------------------------------------------------------
 //
 // KeyframeEffect
 //
 //---------------------------------------------------------------------
 
 KeyframeEffect::KeyframeEffect(nsIDocument* aDocument,
                                const Maybe<OwningAnimationTarget>& aTarget,
-                               const TimingParams& aTiming)
+                               const TimingParams& aTiming,
+                               const KeyframeEffectParams& aOptions)
   : KeyframeEffectReadOnly(aDocument, aTarget,
-                           new AnimationEffectTiming(aDocument, aTiming, this))
+                           new AnimationEffectTiming(aDocument, aTiming, this),
+                           aOptions)
 {
 }
 
 JSObject*
 KeyframeEffect::WrapObject(JSContext* aCx,
                            JS::Handle<JSObject*> aGivenProto)
 {
   return KeyframeEffectBinding::Wrap(aCx, this, aGivenProto);
@@ -1444,55 +1531,38 @@ KeyframeEffect::SetTarget(const Nullable
       nsNodeUtils::AnimationRemoved(mAnimation);
     }
   }
 
   mTarget = newTarget;
 
   if (mTarget) {
     UpdateTargetRegistration();
-    MaybeUpdateProperties();
+    RefPtr<nsStyleContext> styleContext = GetTargetStyleContext();
+    if (styleContext) {
+      UpdateProperties(styleContext);
+    } else if (mEffectOptions.mSpacingMode == SpacingMode::paced) {
+      KeyframeUtils::ApplyDistributeSpacing(mKeyframes);
+    }
 
     RequestRestyle(EffectCompositor::RestyleType::Layer);
 
     nsAutoAnimationMutationBatch mb(mTarget->mElement->OwnerDoc());
     if (mAnimation) {
       nsNodeUtils::AnimationAdded(mAnimation);
     }
+  } else if (mEffectOptions.mSpacingMode == SpacingMode::paced) {
+    // New target is null, so fall back to distribute spacing.
+    KeyframeUtils::ApplyDistributeSpacing(mKeyframes);
   }
 }
 
 KeyframeEffect::~KeyframeEffect()
 {
   // mTiming is cycle collected, so we have to do null check first even though
   // mTiming shouldn't be null during the lifetime of KeyframeEffect.
   if (mTiming) {
     mTiming->Unlink();
   }
 }
 
-void
-KeyframeEffect::MaybeUpdateProperties()
-{
-  if (!mTarget) {
-    return;
-  }
-
-  nsIDocument* doc = mTarget->mElement->OwnerDoc();
-  if (!doc) {
-    return;
-  }
-
-  nsIAtom* pseudo = mTarget->mPseudoType < CSSPseudoElementType::Count ?
-                    nsCSSPseudoElements::GetPseudoAtom(mTarget->mPseudoType) :
-                    nullptr;
-  RefPtr<nsStyleContext> styleContext =
-    nsComputedDOMStyle::GetStyleContextForElement(mTarget->mElement, pseudo,
-                                                  doc->GetShell());
-  if (!styleContext) {
-    return;
-  }
-
-  UpdateProperties(styleContext);
-}
-
 } // namespace dom
 } // namespace mozilla
--- a/dom/animation/KeyframeEffect.h
+++ b/dom/animation/KeyframeEffect.h
@@ -14,19 +14,19 @@
 #include "nsTArray.h"
 #include "nsWrapperCache.h"
 #include "mozilla/AnimationPerformanceWarning.h"
 #include "mozilla/AnimationTarget.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/ComputedTiming.h"
 #include "mozilla/ComputedTimingFunction.h"
 #include "mozilla/EffectCompositor.h"
+#include "mozilla/KeyframeEffectParams.h"
 #include "mozilla/LayerAnimationInfo.h" // LayerAnimations::kRecords
 #include "mozilla/Maybe.h"
-#include "mozilla/OwningNonNull.h"      // OwningNonNull<...>
 #include "mozilla/StickyTimeDuration.h"
 #include "mozilla/StyleAnimationValue.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/TimingParams.h"
 #include "mozilla/dom/AnimationEffectReadOnly.h"
 #include "mozilla/dom/AnimationEffectTimingReadOnly.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/Nullable.h"
@@ -102,17 +102,18 @@ struct Keyframe
     mOffset         = aOther.mOffset;
     mComputedOffset = aOther.mComputedOffset;
     mTimingFunction = Move(aOther.mTimingFunction);
     mPropertyValues = Move(aOther.mPropertyValues);
     return *this;
   }
 
   Maybe<double>                 mOffset;
-  double                        mComputedOffset = 0.0;
+  static MOZ_CONSTEXPR_VAR double kComputedOffsetNotSet = -1.0;
+  double                        mComputedOffset = kComputedOffsetNotSet;
   Maybe<ComputedTimingFunction> mTimingFunction; // Nothing() here means
                                                  // "linear"
   nsTArray<PropertyValuePair>   mPropertyValues;
 };
 
 struct AnimationPropertySegment
 {
   float mFromKey, mToKey;
@@ -191,17 +192,18 @@ namespace dom {
 
 class Animation;
 
 class KeyframeEffectReadOnly : public AnimationEffectReadOnly
 {
 public:
   KeyframeEffectReadOnly(nsIDocument* aDocument,
                          const Maybe<OwningAnimationTarget>& aTarget,
-                         const TimingParams& aTiming);
+                         const TimingParams& aTiming,
+                         const KeyframeEffectParams& aOptions);
 
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(KeyframeEffectReadOnly,
                                                         AnimationEffectReadOnly)
 
   virtual JSObject* WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto) override;
 
@@ -231,18 +233,19 @@ public:
   void GetKeyframes(JSContext*& aCx,
                     nsTArray<JSObject*>& aResult,
                     ErrorResult& aRv);
   void GetProperties(nsTArray<AnimationPropertyDetails>& aProperties,
                      ErrorResult& aRv) const;
 
   IterationCompositeOperation IterationComposite() const;
   CompositeOperation Composite() const;
-  void GetSpacing(nsString& aRetVal) const {
-    aRetVal.AssignLiteral("distribute");
+  void GetSpacing(nsString& aRetVal) const
+  {
+    mEffectOptions.GetSpacingAsString(aRetVal);
   }
 
   already_AddRefed<AnimationEffectTimingReadOnly> Timing() const override;
 
   const TimingParams& SpecifiedTiming() const
   {
     return mTiming->AsTimingParams();
   }
@@ -350,17 +353,18 @@ public:
   // can ignore painting if the animation is not visible.
   // See nsChangeHint_Hints_CanIgnoreIfNotVisible in nsChangeHint.h
   // in detail which change hint can be ignored.
   bool CanIgnoreIfNotVisible() const;
 
 protected:
   KeyframeEffectReadOnly(nsIDocument* aDocument,
                          const Maybe<OwningAnimationTarget>& aTarget,
-                         AnimationEffectTimingReadOnly* aTiming);
+                         AnimationEffectTimingReadOnly* aTiming,
+                         const KeyframeEffectParams& aOptions);
 
   virtual ~KeyframeEffectReadOnly();
 
   template<class KeyframeEffectType, class OptionsType>
   static already_AddRefed<KeyframeEffectType>
   ConstructKeyframeEffect(const GlobalObject& aGlobal,
                           const Nullable<ElementOrCSSPseudoElement>& aTarget,
                           JS::Handle<JSObject*> aKeyframes,
@@ -380,20 +384,30 @@ protected:
   // owning Animation's timing.
   void UpdateTargetRegistration();
 
   // Remove the current effect target from its EffectSet.
   void UnregisterTarget();
 
   void RequestRestyle(EffectCompositor::RestyleType aRestyleType);
 
+  // Looks up the style context associated with the target element, if any.
+  // We need to be careful to *not* call this when we are updating the style
+  // context. That's because calling GetStyleContextForElement when we are in
+  // the process of building a style context may trigger various forms of
+  // infinite recursion.
+  // If aDoc is nullptr, we will use the owner doc of the target element.
+  already_AddRefed<nsStyleContext>
+  GetTargetStyleContext(nsIDocument* aDoc = nullptr);
+
   Maybe<OwningAnimationTarget> mTarget;
   RefPtr<Animation> mAnimation;
 
   RefPtr<AnimationEffectTimingReadOnly> mTiming;
+  KeyframeEffectParams mEffectOptions;
 
   // The specified keyframes.
   nsTArray<Keyframe>          mKeyframes;
 
   // A set of per-property value arrays, derived from |mKeyframes|.
   nsTArray<AnimationProperty> mProperties;
 
   // The computed progress last time we composed the style rule. This is
@@ -424,17 +438,18 @@ private:
   static const TimeDuration OverflowRegionRefreshInterval();
 };
 
 class KeyframeEffect : public KeyframeEffectReadOnly
 {
 public:
   KeyframeEffect(nsIDocument* aDocument,
                  const Maybe<OwningAnimationTarget>& aTarget,
-                 const TimingParams& aTiming);
+                 const TimingParams& aTiming,
+                 const KeyframeEffectParams& aOptions);
 
   JSObject* WrapObject(JSContext* aCx,
                        JS::Handle<JSObject*> aGivenProto) override;
 
   static already_AddRefed<KeyframeEffect>
   Constructor(const GlobalObject& aGlobal,
               const Nullable<ElementOrCSSPseudoElement>& aTarget,
               JS::Handle<JSObject*> aKeyframes,
@@ -448,29 +463,23 @@ public:
   Constructor(const GlobalObject& aGlobal,
               const Nullable<ElementOrCSSPseudoElement>& aTarget,
               JS::Handle<JSObject*> aKeyframes,
               const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
               ErrorResult& aRv);
 
   void NotifySpecifiedTimingUpdated();
 
-  // This method calls MaybeUpdateProperties which is not safe to use when
+  // This method calls GetTargetStyleContext which is not safe to use when
   // we are in the middle of updating style. If we need to use this when
   // updating style, we should pass the nsStyleContext into this method and use
   // that to update the properties rather than calling
   // GetStyleContextForElement.
   void SetTarget(const Nullable<ElementOrCSSPseudoElement>& aTarget);
 
 protected:
   ~KeyframeEffect() override;
-
-  // We need to be careful to *not* call this when we are updating the style
-  // context. That's because calling GetStyleContextForElement when we are in
-  // the process of building a style context may trigger various forms of
-  // infinite recursion.
-  void MaybeUpdateProperties();
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_KeyframeEffect_h
new file mode 100644
--- /dev/null
+++ b/dom/animation/KeyframeEffectParams.cpp
@@ -0,0 +1,160 @@
+/* -*- 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 "mozilla/KeyframeEffectParams.h"
+
+#include "mozilla/KeyframeUtils.h"
+#include "mozilla/RangedPtr.h"
+#include "nsReadableUtils.h"
+
+namespace mozilla {
+
+static inline bool
+IsLetter(char16_t aCh)
+{
+  return (0x41 <= aCh && aCh <= 0x5A) || (0x61 <= aCh && aCh <= 0x7A);
+}
+
+static inline bool
+IsDigit(char16_t aCh)
+{
+  return 0x30 <= aCh && aCh <= 0x39;
+}
+
+static inline bool
+IsNameStartCode(char16_t aCh)
+{
+  return IsLetter(aCh) || aCh >= 0x80 || aCh == '_';
+}
+
+static inline bool
+IsNameCode(char16_t aCh)
+{
+  return IsNameStartCode(aCh) || IsDigit(aCh) || aCh == '-';
+}
+
+static inline bool
+IsNewLine(char16_t aCh)
+{
+  // 0x0A (LF), 0x0C (FF), 0x0D (CR), or pairs of CR followed by LF are
+  // replaced by LF.
+  return aCh == 0x0A || aCh == 0x0C || aCh == 0x0D;
+}
+
+static inline bool
+IsValidEscape(char16_t aFirst, char16_t aSecond)
+{
+  return aFirst == '\\' && !IsNewLine(aSecond);
+}
+
+static bool
+IsIdentStart(RangedPtr<const char16_t> aIter,
+             const char16_t* const aEnd)
+{
+  if (aIter == aEnd) {
+    return false;
+  }
+
+  if (*aIter == '-') {
+    if (aIter + 1 == aEnd) {
+      return false;
+    }
+    char16_t second = *(aIter + 1);
+    return IsNameStartCode(second) ||
+           second == '-' ||
+           (aIter + 2 != aEnd && IsValidEscape(second, *(aIter + 2)));
+  }
+  return IsNameStartCode(*aIter) ||
+         (aIter + 1 != aEnd && IsValidEscape(*aIter, *(aIter + 1)));
+}
+
+static void
+ConsumeIdentToken(RangedPtr<const char16_t>& aIter,
+                  const char16_t* const aEnd,
+                  nsAString& aResult)
+{
+  aResult.Truncate();
+
+  // Check if it starts with an identifier.
+  if (!IsIdentStart(aIter, aEnd)) {
+    return;
+  }
+
+  // Start to consume.
+  while (aIter != aEnd) {
+    if (IsNameCode(*aIter)) {
+      aResult.Append(*aIter);
+    } else if (*aIter == '\\') {
+      const RangedPtr<const char16_t> secondChar = aIter + 1;
+      if (secondChar == aEnd || !IsValidEscape(*aIter, *secondChar)) {
+        break;
+      }
+      // Consume '\\' and append the character following this '\\'.
+      ++aIter;
+      aResult.Append(*aIter);
+    } else {
+      break;
+    }
+    ++aIter;
+  }
+}
+
+/* static */ void
+KeyframeEffectParams::ParseSpacing(const nsAString& aSpacing,
+                                   SpacingMode& aSpacingMode,
+                                   nsCSSProperty& aPacedProperty,
+                                   nsAString& aInvalidPacedProperty,
+                                   ErrorResult& aRv)
+{
+  aInvalidPacedProperty.Truncate();
+
+  // Parse spacing.
+  // distribute | paced({ident})
+  // https://w3c.github.io/web-animations/#dom-keyframeeffectreadonly-spacing
+  // 1. distribute spacing.
+  if (aSpacing.EqualsLiteral("distribute")) {
+    aSpacingMode = SpacingMode::distribute;
+    return;
+  }
+
+  // 2. paced spacing.
+  static const nsLiteralString kPacedPrefix = NS_LITERAL_STRING("paced(");
+  if (!StringBeginsWith(aSpacing, kPacedPrefix)) {
+    aRv.ThrowTypeError<dom::MSG_INVALID_SPACING_MODE_ERROR>(aSpacing);
+    return;
+  }
+
+  RangedPtr<const char16_t> iter(aSpacing.Data() + kPacedPrefix.Length(),
+                                 aSpacing.Data(), aSpacing.Length());
+  const char16_t* const end = aSpacing.EndReading();
+
+  nsAutoString identToken;
+  ConsumeIdentToken(iter, end, identToken);
+  if (identToken.IsEmpty()) {
+    aRv.ThrowTypeError<dom::MSG_INVALID_SPACING_MODE_ERROR>(aSpacing);
+    return;
+  }
+
+  aPacedProperty =
+    nsCSSProps::LookupProperty(identToken, CSSEnabledState::eForAllContent);
+  if (aPacedProperty == eCSSProperty_UNKNOWN ||
+      aPacedProperty == eCSSPropertyExtra_variable ||
+      !KeyframeUtils::IsAnimatableProperty(aPacedProperty)) {
+    aPacedProperty = eCSSProperty_UNKNOWN;
+    aInvalidPacedProperty = identToken;
+  }
+
+  if (end - iter.get() != 1 || *iter != ')') {
+    aRv.ThrowTypeError<dom::MSG_INVALID_SPACING_MODE_ERROR>(aSpacing);
+    return;
+  }
+
+  aSpacingMode = aPacedProperty == eCSSProperty_UNKNOWN
+                 ? SpacingMode::distribute
+                 : SpacingMode::paced;
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/animation/KeyframeEffectParams.h
@@ -0,0 +1,60 @@
+/* -*- 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/. */
+
+#ifndef mozilla_KeyframeEffectParams_h
+#define mozilla_KeyframeEffectParams_h
+
+#include "nsCSSProps.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+enum class SpacingMode
+{
+  distribute,
+  paced
+};
+
+struct KeyframeEffectParams
+{
+  void GetSpacingAsString(nsAString& aSpacing) const
+  {
+    if (mSpacingMode == SpacingMode::distribute) {
+      aSpacing.AssignLiteral("distribute");
+    } else {
+      aSpacing.AssignLiteral("paced(");
+      aSpacing.AppendASCII(nsCSSProps::GetStringValue(mPacedProperty).get());
+      aSpacing.AppendLiteral(")");
+    }
+  }
+
+  /**
+   * Parse spacing string.
+   *
+   * @param aSpacing The input spacing string.
+   * @param [out] aSpacingMode The parsed spacing mode.
+   * @param [out] aPacedProperty The parsed CSS property if using paced spacing.
+   * @param [out] aInvalidPacedProperty A string that, if we parsed a string of
+   *                                    the form 'paced(<ident>)' where <ident>
+   *                                    is not a recognized animatable property,
+   *                                    will be set to <ident>.
+   * @param [out] aRv The error result.
+   */
+  static void ParseSpacing(const nsAString& aSpacing,
+                           SpacingMode& aSpacingMode,
+                           nsCSSProperty& aPacedProperty,
+                           nsAString& aInvalidPacedProperty,
+                           ErrorResult& aRv);
+
+  // FIXME: Bug 1216843: Add IterationCompositeOperations and
+  //        Bug 1216844: Add CompositeOperation
+  SpacingMode mSpacingMode = SpacingMode::distribute;
+  nsCSSProperty mPacedProperty = eCSSProperty_UNKNOWN;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_KeyframeEffectParams_h
--- a/dom/animation/KeyframeUtils.cpp
+++ b/dom/animation/KeyframeUtils.cpp
@@ -3,16 +3,17 @@
  * 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 "mozilla/KeyframeUtils.h"
 
 #include "mozilla/AnimationUtils.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/Move.h"
+#include "mozilla/StyleAnimationValue.h"
 #include "mozilla/TimingParams.h"
 #include "mozilla/dom/BaseKeyframeTypesBinding.h" // For FastBaseKeyframe etc.
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/KeyframeEffect.h"
 #include "mozilla/dom/KeyframeEffectBinding.h"
 #include "jsapi.h" // For ForOfIterator etc.
 #include "nsClassHashtable.h"
 #include "nsCSSParser.h"
@@ -24,16 +25,20 @@
 namespace mozilla {
 
 // ------------------------------------------------------------------
 //
 // Internal data types
 //
 // ------------------------------------------------------------------
 
+// This is used while calculating paced spacing. If the keyframe is not pacable,
+// we set its cumulative distance to kNotPaceable, so we can use this to check.
+const double kNotPaceable = -1.0;
+
 // For the aAllowList parameter of AppendStringOrStringSequence and
 // GetPropertyValuesPairs.
 enum class ListAllowance { eDisallow, eAllow };
 
 /**
  * A comparator to sort nsCSSProperty values such that longhands are sorted
  * before shorthands, and shorthands with less components are sorted before
  * shorthands with more components.
@@ -347,19 +352,16 @@ ConvertKeyframeSequence(JSContext* aCx,
 
 static bool
 GetPropertyValuesPairs(JSContext* aCx,
                        JS::Handle<JSObject*> aObject,
                        ListAllowance aAllowLists,
                        nsTArray<PropertyValuesPair>& aResult);
 
 static bool
-IsAnimatableProperty(nsCSSProperty aProperty);
-
-static bool
 AppendStringOrStringSequenceToArray(JSContext* aCx,
                                     JS::Handle<JS::Value> aValue,
                                     ListAllowance aAllowLists,
                                     nsTArray<nsString>& aValues);
 
 static bool
 AppendValueAsString(JSContext* aCx,
                     nsTArray<nsString>& aValues,
@@ -368,30 +370,50 @@ AppendValueAsString(JSContext* aCx,
 static PropertyValuePair
 MakePropertyValuePair(nsCSSProperty aProperty, const nsAString& aStringValue,
                       nsCSSParser& aParser, nsIDocument* aDocument);
 
 static bool
 HasValidOffsets(const nsTArray<Keyframe>& aKeyframes);
 
 static void
+MarkAsComputeValuesFailureKey(PropertyValuePair& aPair);
+
+static bool
+IsComputeValuesFailureKey(const PropertyValuePair& aPair);
+
+static void
 BuildSegmentsFromValueEntries(nsStyleContext* aStyleContext,
                               nsTArray<KeyframeValueEntry>& aEntries,
                               nsTArray<AnimationProperty>& aResult);
 
 static void
 GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx,
                                            JS::Handle<JS::Value> aValue,
                                            nsTArray<Keyframe>& aResult,
                                            ErrorResult& aRv);
 
 static bool
 RequiresAdditiveAnimation(const nsTArray<Keyframe>& aKeyframes,
                           nsIDocument* aDocument);
 
+static void
+DistributeRange(const Range<Keyframe>& aSpacingRange,
+                const Range<Keyframe>& aRangeToAdjust);
+
+static void
+DistributeRange(const Range<Keyframe>& aSpacingRange);
+
+static void
+PaceRange(const Range<Keyframe>& aKeyframes,
+          const Range<double>& aCumulativeDistances);
+
+static nsTArray<double>
+GetCumulativeDistances(const nsTArray<ComputedKeyframeValues>& aValues,
+                       nsCSSProperty aProperty);
 
 // ------------------------------------------------------------------
 //
 // Public API
 //
 // ------------------------------------------------------------------
 
 /* static */ nsTArray<Keyframe>
@@ -448,82 +470,163 @@ KeyframeUtils::GetKeyframesFromObject(JS
     aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR);
     keyframes.Clear();
   }
 
   return keyframes;
 }
 
 /* static */ void
-KeyframeUtils::ApplyDistributeSpacing(nsTArray<Keyframe>& aKeyframes)
+KeyframeUtils::ApplySpacing(nsTArray<Keyframe>& aKeyframes,
+                            SpacingMode aSpacingMode,
+                            nsCSSProperty aProperty,
+                            nsTArray<ComputedKeyframeValues>& aComputedValues)
 {
   if (aKeyframes.IsEmpty()) {
     return;
   }
 
-  // If the first or last keyframes have an unspecified offset,
-  // fill them in with 0% and 100%.  If there is only a single keyframe,
-  // then it gets 100%.
-  Keyframe& lastElement = aKeyframes.LastElement();
-  lastElement.mComputedOffset = lastElement.mOffset.valueOr(1.0);
+  nsTArray<double> cumulativeDistances;
+  if (aSpacingMode == SpacingMode::paced) {
+    MOZ_ASSERT(IsAnimatableProperty(aProperty),
+               "Paced property should be animatable");
+
+    cumulativeDistances = GetCumulativeDistances(aComputedValues, aProperty);
+    // Reset the computed offsets if using paced spacing.
+    for (Keyframe& keyframe : aKeyframes) {
+      keyframe.mComputedOffset = Keyframe::kComputedOffsetNotSet;
+    }
+  }
+
+  // If the first keyframe has an unspecified offset, fill it in with 0%.
+  // If there is only a single keyframe, then it gets 100%.
   if (aKeyframes.Length() > 1) {
     Keyframe& firstElement = aKeyframes[0];
     firstElement.mComputedOffset = firstElement.mOffset.valueOr(0.0);
+    // We will fill in the last keyframe's offset below
+  } else {
+    Keyframe& lastElement = aKeyframes.LastElement();
+    lastElement.mComputedOffset = lastElement.mOffset.valueOr(1.0);
   }
 
   // Fill in remaining missing offsets.
-  size_t i = 0;
-  while (i < aKeyframes.Length() - 1) {
-    double start = aKeyframes[i].mComputedOffset;
-    size_t j = i + 1;
-    while (aKeyframes[j].mOffset.isNothing() && j < aKeyframes.Length() - 1) {
-      ++j;
+  const Keyframe* const last = aKeyframes.cend() - 1;
+  const RangedPtr<Keyframe> begin(aKeyframes.begin(), aKeyframes.Length());
+  RangedPtr<Keyframe> keyframeA = begin;
+  while (keyframeA != last) {
+    // Find keyframe A and keyframe B *between* which we will apply spacing.
+    RangedPtr<Keyframe> keyframeB = keyframeA + 1;
+    while (keyframeB->mOffset.isNothing() && keyframeB != last) {
+      ++keyframeB;
     }
-    double end = aKeyframes[j].mOffset.valueOr(1.0);
-    size_t n = j - i;
-    for (size_t k = 1; k < n; ++k) {
-      double offset = start + double(k) / n * (end - start);
-      aKeyframes[i + k].mComputedOffset = offset;
+    keyframeB->mComputedOffset = keyframeB->mOffset.valueOr(1.0);
+
+    // Fill computed offsets in (keyframe A, keyframe B).
+    if (aSpacingMode == SpacingMode::distribute) {
+      // Bug 1276573: Use the new constructor accepting two RangedPtr<T>
+      // arguments, so we can make the code simpler.
+      DistributeRange(Range<Keyframe>(keyframeA.get(),
+                                      keyframeB - keyframeA + 1));
+    } else {
+      // a) Find Paced A (first paceable keyframe) and
+      //    Paced B (last paceable keyframe) in [keyframe A, keyframe B].
+      RangedPtr<Keyframe> pacedA = keyframeA;
+      while (pacedA < keyframeB &&
+             cumulativeDistances[pacedA - begin] == kNotPaceable) {
+        ++pacedA;
+      }
+      RangedPtr<Keyframe> pacedB = keyframeB;
+      while (pacedB > keyframeA &&
+             cumulativeDistances[pacedB - begin] == kNotPaceable) {
+        --pacedB;
+      }
+      // As spec says, if there is no paceable keyframe
+      // in [keyframe A, keyframe B], we let Paced A and Paced B refer to
+      // keyframe B.
+      if (pacedA > pacedB) {
+        pacedA = pacedB = keyframeB;
+      }
+      // b) Apply distributing offsets in (keyframe A, Paced A] and
+      //    [Paced B, keyframe B).
+      DistributeRange(Range<Keyframe>(keyframeA.get(),
+                                      keyframeB - keyframeA + 1),
+                      Range<Keyframe>((keyframeA + 1).get(),
+                                      pacedA - keyframeA));
+      DistributeRange(Range<Keyframe>(keyframeA.get(),
+                                      keyframeB - keyframeA + 1),
+                      Range<Keyframe>(pacedB.get(),
+                                      keyframeB - pacedB));
+      // c) Apply paced offsets to each paceable keyframe in (Paced A, Paced B).
+      //    We pass the range [Paced A, Paced B] since PaceRange needs the end
+      //    points of the range in order to calculate the correct offset.
+      PaceRange(Range<Keyframe>(pacedA.get(), pacedB - pacedA + 1),
+                Range<double>(&cumulativeDistances[pacedA - begin],
+                              pacedB - pacedA + 1));
+      // d) Fill in any computed offsets in (Paced A, Paced B) that are still
+      //    not set (e.g. because the keyframe was not paceable, or because the
+      //    cumulative distance between paceable properties was zero).
+      for (RangedPtr<Keyframe> frame = pacedA + 1; frame < pacedB; ++frame) {
+        if (frame->mComputedOffset != Keyframe::kComputedOffsetNotSet) {
+          continue;
+        }
+
+        RangedPtr<Keyframe> start = frame - 1;
+        RangedPtr<Keyframe> end = frame + 1;
+        while (end < pacedB &&
+               end->mComputedOffset == Keyframe::kComputedOffsetNotSet) {
+          ++end;
+        }
+        DistributeRange(Range<Keyframe>(start.get(), end - start + 1));
+        frame = end;
+      }
     }
-    i = j;
-    aKeyframes[j].mComputedOffset = end;
+    keyframeA = keyframeB;
   }
 }
 
-/* static */ nsTArray<AnimationProperty>
-KeyframeUtils::GetAnimationPropertiesFromKeyframes(
-    nsStyleContext* aStyleContext,
-    dom::Element* aElement,
-    CSSPseudoElementType aPseudoType,
-    const nsTArray<Keyframe>& aFrames)
+/* static */ void
+KeyframeUtils::ApplyDistributeSpacing(nsTArray<Keyframe>& aKeyframes)
+{
+  nsTArray<ComputedKeyframeValues> emptyArray;
+  ApplySpacing(aKeyframes, SpacingMode::distribute, eCSSProperty_UNKNOWN,
+               emptyArray);
+}
+
+/* static */ nsTArray<ComputedKeyframeValues>
+KeyframeUtils::GetComputedKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
+                                         dom::Element* aElement,
+                                         nsStyleContext* aStyleContext)
 {
   MOZ_ASSERT(aStyleContext);
   MOZ_ASSERT(aElement);
 
-  nsTArray<KeyframeValueEntry> entries;
+  const size_t len = aKeyframes.Length();
+  nsTArray<ComputedKeyframeValues> result(len);
 
-  for (const Keyframe& frame : aFrames) {
+  for (const Keyframe& frame : aKeyframes) {
     nsCSSPropertySet propertiesOnThisKeyframe;
+    ComputedKeyframeValues* computedValues = result.AppendElement();
     for (const PropertyValuePair& pair :
            PropertyPriorityIterator(frame.mPropertyValues)) {
       if (IsInvalidValuePair(pair)) {
         continue;
       }
 
       // Expand each value into the set of longhands and produce
       // a KeyframeValueEntry for each value.
       nsTArray<PropertyStyleAnimationValuePair> values;
 
       // For shorthands, we store the string as a token stream so we need to
       // extract that first.
       if (nsCSSProps::IsShorthand(pair.mProperty)) {
         nsCSSValueTokenStream* tokenStream = pair.mValue.GetTokenStreamValue();
         if (!StyleAnimationValue::ComputeValues(pair.mProperty,
               CSSEnabledState::eForAllContent, aElement, aStyleContext,
-              tokenStream->mTokenStream, /* aUseSVGMode */ false, values)) {
+              tokenStream->mTokenStream, /* aUseSVGMode */ false, values) ||
+            IsComputeValuesFailureKey(pair)) {
           continue;
         }
       } else {
         if (!StyleAnimationValue::ComputeValues(pair.mProperty,
               CSSEnabledState::eForAllContent, aElement, aStyleContext,
               pair.mValue, /* aUseSVGMode */ false, values)) {
           continue;
         }
@@ -533,34 +636,76 @@ KeyframeUtils::GetAnimationPropertiesFro
       }
 
       for (auto& value : values) {
         // If we already got a value for this property on the keyframe,
         // skip this one.
         if (propertiesOnThisKeyframe.HasProperty(value.mProperty)) {
           continue;
         }
-
-        KeyframeValueEntry* entry = entries.AppendElement();
-        entry->mOffset = frame.mComputedOffset;
-        entry->mProperty = value.mProperty;
-        entry->mValue = value.mValue;
-        entry->mTimingFunction = frame.mTimingFunction;
-
+        computedValues->AppendElement(value);
         propertiesOnThisKeyframe.AddProperty(value.mProperty);
       }
     }
   }
 
-  nsTArray<AnimationProperty> result;
-  BuildSegmentsFromValueEntries(aStyleContext, entries, result);
-
+  MOZ_ASSERT(result.Length() == aKeyframes.Length(), "Array length mismatch");
   return result;
 }
 
+/* static */ nsTArray<AnimationProperty>
+KeyframeUtils::GetAnimationPropertiesFromKeyframes(
+  const nsTArray<Keyframe>& aKeyframes,
+  const nsTArray<ComputedKeyframeValues>& aComputedValues,
+  nsStyleContext* aStyleContext)
+{
+  MOZ_ASSERT(aKeyframes.Length() == aComputedValues.Length(),
+             "Array length mismatch");
+
+  nsTArray<KeyframeValueEntry> entries(aKeyframes.Length());
+
+  const size_t len = aKeyframes.Length();
+  for (size_t i = 0; i < len; ++i) {
+    const Keyframe& frame = aKeyframes[i];
+    for (auto& value : aComputedValues[i]) {
+      MOZ_ASSERT(frame.mComputedOffset != Keyframe::kComputedOffsetNotSet,
+                 "Invalid computed offset");
+      KeyframeValueEntry* entry = entries.AppendElement();
+      entry->mOffset = frame.mComputedOffset;
+      entry->mProperty = value.mProperty;
+      entry->mValue = value.mValue;
+      entry->mTimingFunction = frame.mTimingFunction;
+    }
+  }
+
+  nsTArray<AnimationProperty> result;
+  BuildSegmentsFromValueEntries(aStyleContext, entries, result);
+  return result;
+}
+
+/* static */ bool
+KeyframeUtils::IsAnimatableProperty(nsCSSProperty aProperty)
+{
+  if (aProperty == eCSSProperty_UNKNOWN) {
+    return false;
+  }
+
+  if (!nsCSSProps::IsShorthand(aProperty)) {
+    return nsCSSProps::kAnimTypeTable[aProperty] != eStyleAnimType_None;
+  }
+
+  CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, aProperty,
+                                       CSSEnabledState::eForAllContent) {
+    if (nsCSSProps::kAnimTypeTable[*subprop] != eStyleAnimType_None) {
+      return true;
+    }
+  }
+
+  return false;
+}
 
 // ------------------------------------------------------------------
 //
 // Internal helpers
 //
 // ------------------------------------------------------------------
 
 /**
@@ -672,16 +817,27 @@ ConvertKeyframeSequence(JSContext* aCx,
         return false;
       }
     }
 
     for (PropertyValuesPair& pair : propertyValuePairs) {
       MOZ_ASSERT(pair.mValues.Length() == 1);
       keyframe->mPropertyValues.AppendElement(
         MakePropertyValuePair(pair.mProperty, pair.mValues[0], parser, doc));
+
+      // When we go to convert keyframes into arrays of property values we
+      // call StyleAnimation::ComputeValues. This should normally return true
+      // but in order to test the case where it does not, BaseKeyframeDict
+      // includes a chrome-only member that can be set to indicate that
+      // ComputeValues should fail for shorthand property values on that
+      // keyframe.
+      if (nsCSSProps::IsShorthand(pair.mProperty) &&
+          keyframeDict.mSimulateComputeValuesFailure) {
+        MarkAsComputeValuesFailureKey(keyframe->mPropertyValues.LastElement());
+      }
     }
   }
 
   return true;
 }
 
 /**
  * Reads the property-values pairs from the specified JS object.
@@ -716,17 +872,17 @@ GetPropertyValuesPairs(JSContext* aCx,
   for (size_t i = 0, n = ids.length(); i < n; i++) {
     nsAutoJSString propName;
     if (!propName.init(aCx, ids[i])) {
       return false;
     }
     nsCSSProperty property =
       nsCSSProps::LookupPropertyByIDLName(propName,
                                           CSSEnabledState::eForAllContent);
-    if (IsAnimatableProperty(property)) {
+    if (KeyframeUtils::IsAnimatableProperty(property)) {
       AdditionalProperty* p = properties.AppendElement();
       p->mProperty = property;
       p->mJsidIndex = i;
     }
   }
 
   // Sort the entries by IDL name and then get each value and
   // convert it either to a DOMString or to a
@@ -746,41 +902,16 @@ GetPropertyValuesPairs(JSContext* aCx,
       return false;
     }
   }
 
   return true;
 }
 
 /**
- * Returns true if |aProperty| or, for shorthands, one or more of
- * |aProperty|'s subproperties, is animatable.
- */
-static bool
-IsAnimatableProperty(nsCSSProperty aProperty)
-{
-  if (aProperty == eCSSProperty_UNKNOWN) {
-    return false;
-  }
-
-  if (!nsCSSProps::IsShorthand(aProperty)) {
-    return nsCSSProps::kAnimTypeTable[aProperty] != eStyleAnimType_None;
-  }
-
-  CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, aProperty,
-                                       CSSEnabledState::eForAllContent) {
-    if (nsCSSProps::kAnimTypeTable[*subprop] != eStyleAnimType_None) {
-      return true;
-    }
-  }
-
-  return false;
-}
-
-/**
  * Converts aValue to DOMString, if aAllowLists is eDisallow, or
  * to (DOMString or sequence<DOMString>) if aAllowLists is aAllow.
  * The resulting strings are appended to aValues.
  */
 static bool
 AppendStringOrStringSequenceToArray(JSContext* aCx,
                                     JS::Handle<JS::Value> aValue,
                                     ListAllowance aAllowLists,
@@ -904,16 +1035,61 @@ HasValidOffsets(const nsTArray<Keyframe>
         return false;
       }
       offset = thisOffset;
     }
   }
   return true;
 }
 
+/**
+ * Takes a property-value pair for a shorthand property and modifies the
+ * value to indicate that when we call StyleAnimationValue::ComputeValues on
+ * that value we should behave as if that function had failed.
+ *
+ * @param aPair The PropertyValuePair to modify. |aPair.mProperty| must be
+ *              a shorthand property.
+ */
+static void
+MarkAsComputeValuesFailureKey(PropertyValuePair& aPair)
+{
+  MOZ_ASSERT(nsCSSProps::IsShorthand(aPair.mProperty),
+             "Only shorthand property values can be marked as failure values");
+
+  // We store shorthand values as nsCSSValueTokenStream objects whose mProperty
+  // and mShorthandPropertyID are eCSSProperty_UNKNOWN and whose mTokenStream
+  // member contains the shorthand property's value as a string.
+  //
+  // We need to leave mShorthandPropertyID as eCSSProperty_UNKNOWN so that
+  // nsCSSValue::AppendToString returns the mTokenStream value, but we can
+  // update mPropertyID to a special value to indicate that this is
+  // a special failure sentinel.
+  nsCSSValueTokenStream* tokenStream = aPair.mValue.GetTokenStreamValue();
+  MOZ_ASSERT(tokenStream->mPropertyID == eCSSProperty_UNKNOWN,
+             "Shorthand value should initially have an unknown property ID");
+  tokenStream->mPropertyID = eCSSPropertyExtra_no_properties;
+}
+
+/**
+ * Returns true if |aPair| is a property-value pair on which we have
+ * previously called MarkAsComputeValuesFailureKey (and hence we should
+ * simulate failure when calling StyleAnimationValue::ComputeValues using its
+ * value).
+ *
+ * @param aPair The property-value pair to test.
+ * @return True if |aPair| represents a failure value.
+ */
+static bool
+IsComputeValuesFailureKey(const PropertyValuePair& aPair)
+{
+  return nsCSSProps::IsShorthand(aPair.mProperty) &&
+         aPair.mValue.GetTokenStreamValue()->mPropertyID ==
+           eCSSPropertyExtra_no_properties;
+}
+
 static already_AddRefed<nsStyleContext>
 CreateStyleContextForAnimationValue(nsCSSProperty aProperty,
                                     StyleAnimationValue aValue,
                                     nsStyleContext* aBaseStyleContext)
 {
   MOZ_ASSERT(aBaseStyleContext,
              "CreateStyleContextForAnimationValue needs to be called "
              "with a valid nsStyleContext");
@@ -1228,17 +1404,17 @@ RequiresAdditiveAnimation(const nsTArray
     } else if (aOffset == 1.0) {
       propertiesWithToValue.AddProperty(aProperty);
     }
   };
 
   for (size_t i = 0, len = aKeyframes.Length(); i < len; i++) {
     const Keyframe& frame = aKeyframes[i];
 
-    // We won't have called ApplyDistributeSpacing when this is called so
+    // We won't have called ApplySpacing when this is called so
     // we can't use frame.mComputedOffset. Instead we do a rough version
     // of that algorithm that substitutes null offsets with 0.0 for the first
     // frame, 1.0 for the last frame, and 0.5 for everything else.
     double computedOffset = i == len - 1
                             ? 1.0
                             : i == 0 ? 0.0 : 0.5;
     double offsetToUse = frame.mOffset
                          ? frame.mOffset.value()
@@ -1265,9 +1441,209 @@ RequiresAdditiveAnimation(const nsTArray
       }
     }
   }
 
   return !propertiesWithFromValue.Equals(properties) ||
          !propertiesWithToValue.Equals(properties);
 }
 
+/**
+ * Evenly distribute the computed offsets in (A, B).
+ * We pass the range keyframes in [A, B] and use A, B to calculate distributing
+ * computed offsets in (A, B). The second range, aRangeToAdjust, is passed, so
+ * we can know which keyframe we want to apply to. aRangeToAdjust should be in
+ * the range of aSpacingRange.
+ *
+ * @param aSpacingRange The sequence of keyframes between whose endpoints we
+ *   should apply distribute spacing.
+ * @param aRangeToAdjust The range of keyframes we want to apply to.
+ */
+static void
+DistributeRange(const Range<Keyframe>& aSpacingRange,
+                const Range<Keyframe>& aRangeToAdjust)
+{
+  MOZ_ASSERT(aRangeToAdjust.start() >= aSpacingRange.start() &&
+             aRangeToAdjust.end() <= aSpacingRange.end(),
+             "Out of range");
+  const size_t n = aSpacingRange.length() - 1;
+  const double startOffset = aSpacingRange[0].mComputedOffset;
+  const double diffOffset = aSpacingRange[n].mComputedOffset - startOffset;
+  for (auto iter = aRangeToAdjust.start();
+       iter != aRangeToAdjust.end();
+       ++iter) {
+    size_t index = iter - aSpacingRange.start();
+    iter->mComputedOffset = startOffset + double(index) / n * diffOffset;
+  }
+}
+
+/**
+ * Overload of DistributeRange to apply distribute spacing to all keyframes in
+ * between the endpoints of the given range.
+ *
+ * @param aSpacingRange The sequence of keyframes between whose endpoints we
+ *   should apply distribute spacing.
+ */
+static void
+DistributeRange(const Range<Keyframe>& aSpacingRange)
+{
+  // We don't need to apply distribute spacing to keyframe A and keyframe B.
+  DistributeRange(aSpacingRange,
+                  Range<Keyframe>((aSpacingRange.start() + 1).get(),
+                                  aSpacingRange.end() - aSpacingRange.start()
+                                    - 2));
+}
+
+/**
+ * Apply paced spacing to all paceable keyframes in between the endpoints of the
+ * given range.
+ *
+ * @param aKeyframes The range of keyframes between whose endpoints we should
+ *   apply paced spacing. Both endpoints should be paceable, i.e. the
+ *   corresponding elements in |aCumulativeDist| should not be kNotPaceable.
+ *   Within this function, we refer to the start and end points of this range
+ *   as Paced A and Paced B respectively in keeping with the notation used in
+ *   the spec.
+ * @param aCumulativeDistances The sequence of cumulative distances of the paced
+ *   property as returned by GetCumulativeDistances(). This acts as a
+ *   parallel range to |aKeyframes|.
+ */
+static void
+PaceRange(const Range<Keyframe>& aKeyframes,
+          const Range<double>& aCumulativeDistances)
+{
+  MOZ_ASSERT(aKeyframes.length() == aCumulativeDistances.length(),
+             "Range length mismatch");
+
+  const size_t len = aKeyframes.length();
+  // If there is nothing between the end points, there is nothing to space.
+  if (len < 3) {
+    return;
+  }
+
+  const double distA = *(aCumulativeDistances.start());
+  const double distB = *(aCumulativeDistances.end() - 1);
+  MOZ_ASSERT(distA != kNotPaceable && distB != kNotPaceable,
+             "Both Paced A and Paced B should be paceable");
+
+  // If the total distance is zero, we should fall back to distribute spacing.
+  // The caller will fill-in any keyframes without a computed offset using
+  // distribute spacing so we can just return here.
+  if (distA == distB) {
+    return;
+  }
+
+  const RangedPtr<Keyframe> pacedA = aKeyframes.start();
+  const RangedPtr<Keyframe> pacedB = aKeyframes.end() - 1;
+  MOZ_ASSERT(pacedA->mComputedOffset != Keyframe::kComputedOffsetNotSet &&
+             pacedB->mComputedOffset != Keyframe::kComputedOffsetNotSet,
+             "Both Paced A and Paced B should have valid computed offsets");
+
+  // Apply computed offset.
+  const double offsetA     = pacedA->mComputedOffset;
+  const double diffOffset  = pacedB->mComputedOffset - offsetA;
+  const double initialDist = distA;
+  const double totalDist   = distB - initialDist;
+  for (auto iter = pacedA + 1; iter != pacedB; ++iter) {
+    size_t k = iter - aKeyframes.start();
+    if (aCumulativeDistances[k] == kNotPaceable) {
+      continue;
+    }
+
+    double dist = aCumulativeDistances[k] - initialDist;
+    iter->mComputedOffset = offsetA + diffOffset * dist / totalDist;
+  }
+}
+
+/**
+ * Get cumulative distances for the paced property.
+ *
+ * @param aValues The computed values returned by GetComputedKeyframeValues.
+ * @param aPacedProperty The paced property.
+ * @return The cumulative distances for the paced property. The length will be
+ *   the same as aValues.
+ */
+static nsTArray<double>
+GetCumulativeDistances(const nsTArray<ComputedKeyframeValues>& aValues,
+                       nsCSSProperty aPacedProperty)
+{
+  // a) If aPacedProperty is a shorthand property, get its components.
+  //    Otherwise, just add the longhand property into the set.
+  size_t pacedPropertyCount = 0;
+  nsCSSPropertySet pacedPropertySet;
+  bool isShorthand = nsCSSProps::IsShorthand(aPacedProperty);
+  if (isShorthand) {
+    CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(p, aPacedProperty,
+                                         CSSEnabledState::eForAllContent) {
+      pacedPropertySet.AddProperty(*p);
+      ++pacedPropertyCount;
+    }
+  } else {
+    pacedPropertySet.AddProperty(aPacedProperty);
+    pacedPropertyCount = 1;
+  }
+
+  // b) Search each component (shorthand) or the longhand property, and
+  //    calculate the cumulative distances of paceable keyframe pairs.
+  const size_t len = aValues.Length();
+  nsTArray<double> cumulativeDistances(len);
+  // cumulativeDistances is a parallel array to |aValues|, so set its length to
+  // the length of |aValues|.
+  cumulativeDistances.SetLength(len);
+  ComputedKeyframeValues prevPacedValues;
+  size_t preIdx = 0;
+  for (size_t i = 0; i < len; ++i) {
+    // Find computed values of the paced property.
+    ComputedKeyframeValues pacedValues;
+    for (const PropertyStyleAnimationValuePair& pair : aValues[i]) {
+      if (pacedPropertySet.HasProperty(pair.mProperty)) {
+        pacedValues.AppendElement(pair);
+      }
+    }
+
+    // Check we have values for all the paceable longhand components.
+    if (pacedValues.Length() != pacedPropertyCount) {
+      // This keyframe is not paceable, assign kNotPaceable and skip it.
+      cumulativeDistances[i] = kNotPaceable;
+      continue;
+    }
+
+    if (prevPacedValues.IsEmpty()) {
+      // This is the first paceable keyframe so its cumulative distance is 0.0.
+      cumulativeDistances[i] = 0.0;
+    } else {
+      double dist = 0.0;
+      if (isShorthand) {
+        // Apply the distance by the square root of the sum of squares of
+        // longhand component distances.
+        for (size_t propIdx = 0; propIdx < pacedPropertyCount; ++propIdx) {
+          nsCSSProperty prop = prevPacedValues[propIdx].mProperty;
+          MOZ_ASSERT(pacedValues[propIdx].mProperty == prop,
+                     "Property mismatch");
+
+          double componentDistance = 0.0;
+          if (StyleAnimationValue::ComputeDistance(
+                prop,
+                prevPacedValues[propIdx].mValue,
+                pacedValues[propIdx].mValue,
+                componentDistance)) {
+            dist += componentDistance * componentDistance;
+          }
+        }
+        dist = sqrt(dist);
+      } else {
+        // If the property is longhand, we just use the 1st value.
+        // If ComputeDistance() fails, |dist| will remain zero so there will be
+        // no distance between the previous paced value and this value.
+        StyleAnimationValue::ComputeDistance(aPacedProperty,
+                                             prevPacedValues[0].mValue,
+                                             pacedValues[0].mValue,
+                                             dist);
+      }
+      cumulativeDistances[i] = cumulativeDistances[preIdx] + dist;
+    }
+    prevPacedValues.SwapElements(pacedValues);
+    preIdx = i;
+  }
+  return cumulativeDistances;
+}
+
 } // namespace mozilla
--- a/dom/animation/KeyframeUtils.h
+++ b/dom/animation/KeyframeUtils.h
@@ -13,25 +13,30 @@
 struct JSContext;
 class JSObject;
 
 namespace mozilla {
 struct AnimationProperty;
 enum class CSSPseudoElementType : uint8_t;
 class ErrorResult;
 struct Keyframe;
+struct PropertyStyleAnimationValuePair;
 
 namespace dom {
 class Element;
 } // namespace dom
 } // namespace mozilla
 
 
 namespace mozilla {
 
+// Represents the set of property-value pairs on a Keyframe converted to
+// computed values.
+using ComputedKeyframeValues = nsTArray<PropertyStyleAnimationValuePair>;
+
 /**
  * Utility methods for processing keyframes.
  */
 class KeyframeUtils
 {
 public:
   /**
    * Converts a JS value representing a property-indexed keyframe or a sequence
@@ -47,39 +52,92 @@ public:
    *   returned.
    */
   static nsTArray<Keyframe>
   GetKeyframesFromObject(JSContext* aCx,
                          JS::Handle<JSObject*> aFrames,
                          ErrorResult& aRv);
 
   /**
-   * Fills in the mComputedOffset member of each keyframe in the given array
-   * using the "distribute" spacing algorithm.
+   * Calculate the StyleAnimationValues of properties of each keyframe.
+   * This involves expanding shorthand properties into longhand properties,
+   * removing the duplicated properties for each keyframe, and creating an
+   * array of |property:computed value| pairs for each keyframe.
+   *
+   * These computed values are used *both* when computing the final set of
+   * per-property animation values (see GetAnimationPropertiesFromKeyframes) as
+   * well when applying paced spacing. By returning these values here, we allow
+   * the result to be re-used in both operations.
    *
-   * http://w3c.github.io/web-animations/#distribute-keyframe-spacing-mode
+   * @param aKeyframes The input keyframes.
+   * @param aElement The context element.
+   * @param aStyleContext The style context to use when computing values.
+   * @return The set of ComputedKeyframeValues. The length will be the same as
+   *   aFrames.
+   */
+  static nsTArray<ComputedKeyframeValues>
+  GetComputedKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
+                            dom::Element* aElement,
+                            nsStyleContext* aStyleContext);
+
+  /**
+   * Fills in the mComputedOffset member of each keyframe in the given array
+   * using the specified spacing mode.
+   *
+   * https://w3c.github.io/web-animations/#spacing-keyframes
    *
-   * @param keyframes The set of keyframes to adjust.
+   * @param aKeyframes The set of keyframes to adjust.
+   * @param aSpacingMode The spacing mode to apply.
+   * @param aProperty The paced property. Only used when |aSpacingMode| is
+   *   SpacingMode::paced. In all other cases it is ignored and hence may be
+   *   any value, e.g. eCSSProperty_UNKNOWN.
+   * @param aComputedValues The set of computed keyframe values as returned by
+   *   GetComputedKeyframeValues. Only used when |aSpacingMode| is
+   *   SpacingMode::paced. In all other cases this parameter is unused and may
+   *   be any value including an empty array.
+   */
+  static void ApplySpacing(nsTArray<Keyframe>& aKeyframes,
+                           SpacingMode aSpacingMode,
+                           nsCSSProperty aProperty,
+                           nsTArray<ComputedKeyframeValues>& aComputedValues);
+
+  /**
+   * Wrapper for ApplySpacing to simplify using distribute spacing.
+   *
+   * @param aKeyframes The set of keyframes to adjust.
    */
   static void ApplyDistributeSpacing(nsTArray<Keyframe>& aKeyframes);
 
   /**
    * Converts an array of Keyframe objects into an array of AnimationProperty
-   * objects. This involves expanding shorthand properties into longhand
-   * properties, creating an array of computed values for each longhand
-   * property and determining the offset and timing function to use for each
-   * value.
+   * objects. This involves creating an array of computed values for each
+   * longhand property and determining the offset and timing function to use
+   * for each value.
    *
-   * @param aStyleContext The style context to use when computing values.
-   * @param aFrames The input keyframes.
+   * @param aKeyframes The input keyframes.
+   * @param aComputedValues The computed keyframe values (as returned by
+   *   GetComputedKeyframeValues) used to fill in the individual
+   *   AnimationPropertySegment objects. Although these values could be
+   *   calculated from |aKeyframes|, passing them in as a separate parameter
+   *   allows the result of GetComputedKeyframeValues to be re-used both
+   *   here and in ApplySpacing.
+   * @param aStyleContext The style context to calculate the style difference.
    * @return The set of animation properties. If an error occurs, the returned
    *   array will be empty.
    */
-  static nsTArray<AnimationProperty>
-  GetAnimationPropertiesFromKeyframes(nsStyleContext* aStyleContext,
-                                      dom::Element* aElement,
-                                      CSSPseudoElementType aPseudoType,
-                                      const nsTArray<Keyframe>& aFrames);
+  static nsTArray<AnimationProperty> GetAnimationPropertiesFromKeyframes(
+    const nsTArray<Keyframe>& aKeyframes,
+    const nsTArray<ComputedKeyframeValues>& aComputedValues,
+    nsStyleContext* aStyleContext);
+
+  /**
+   * Check if the property or, for shorthands, one or more of
+   * its subproperties, is animatable.
+   *
+   * @param aProperty The property to check.
+   * @return true if |aProperty| is animatable.
+   */
+  static bool IsAnimatableProperty(nsCSSProperty aProperty);
 };
 
 } // namespace mozilla
 
 #endif // mozilla_KeyframeUtils_h
--- a/dom/animation/moz.build
+++ b/dom/animation/moz.build
@@ -23,16 +23,17 @@ EXPORTS.mozilla += [
     'AnimationPerformanceWarning.h',
     'AnimationTarget.h',
     'AnimationUtils.h',
     'AnimValuesStyleRule.h',
     'ComputedTiming.h',
     'ComputedTimingFunction.h',
     'EffectCompositor.h',
     'EffectSet.h',
+    'KeyframeEffectParams.h',
     'KeyframeUtils.h',
     'PendingAnimationTracker.h',
     'PseudoElementHashEntry.h',
     'TimingParams.h',
 ]
 
 UNIFIED_SOURCES += [
     'Animation.cpp',
@@ -44,16 +45,17 @@ UNIFIED_SOURCES += [
     'AnimationUtils.cpp',
     'AnimValuesStyleRule.cpp',
     'ComputedTimingFunction.cpp',
     'CSSPseudoElement.cpp',
     'DocumentTimeline.cpp',
     'EffectCompositor.cpp',
     'EffectSet.cpp',
     'KeyframeEffect.cpp',
+    'KeyframeEffectParams.cpp',
     'KeyframeUtils.cpp',
     'PendingAnimationTracker.cpp',
     'TimingParams.cpp',
 ]
 
 LOCAL_INCLUDES += [
     '/dom/base',
     '/layout/base',
--- a/dom/animation/test/chrome/test_animation_properties.html
+++ b/dom/animation/test/chrome/test_animation_properties.html
@@ -500,16 +500,17 @@ var gTests = [
                             value(1, 'calc(100px + 10%)', 'replace') ] } ]
   },
 
   // ---------------------------------------------------------------------
   //
   // Tests for CSS variable handling conversion
   //
   // ---------------------------------------------------------------------
+
   { desc:     'CSS variables are resolved to their corresponding values',
     frames:   { left: ['10px', 'var(--var-100px)'] },
     expected: [ { property: 'left',
                   values: [ value(0, '10px', 'replace', 'linear'),
                             value(1, '100px', 'replace') ] } ]
   },
   { desc:     'CSS variables in calc() expressions are resolved',
     frames:   { left: ['10px', 'calc(var(--var-100px) / 2 - 10%)'] },
@@ -528,16 +529,300 @@ var gTests = [
                             value(1, '200px', 'replace') ] },
                 { property: 'margin-bottom',
                   values: [ value(0, '10px', 'replace', 'linear'),
                             value(1, '100px', 'replace') ] },
                 { property: 'margin-left',
                   values: [ value(0, '10px', 'replace', 'linear'),
                             value(1, '200px', 'replace') ] } ]
   },
+
+  // ---------------------------------------------------------------------
+  //
+  // Tests for properties that parse correctly but which we fail to
+  // convert to computed values.
+  //
+  // ---------------------------------------------------------------------
+
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' initial keyframe',
+    frames:   [ { margin: '5px', simulateComputeValuesFailure: true },
+                { margin: '5px' } ],
+    expected: [  ]
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' initial keyframe where we have enough values to create'
+              + ' a final segment',
+    frames:   [ { margin: '5px', simulateComputeValuesFailure: true },
+                { margin: '5px' },
+                { margin: '5px' } ],
+    expected: [  ]
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' initial overlapping keyframes (first in series of two)',
+    frames:   [ { margin: '5px', offset: 0,
+                  simulateComputeValuesFailure: true },
+                { margin: '5px', offset: 0 },
+                { margin: '5px' } ],
+    expected: [ { property: 'margin-top',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-right',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-bottom',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-left',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] } ]
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' initial overlapping keyframes (second in series of two)',
+    frames:   [ { margin: '5px', offset: 0 },
+                { margin: '5px', offset: 0,
+                  simulateComputeValuesFailure: true },
+                { margin: '5px' } ],
+    expected: [ { property: 'margin-top',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-right',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-bottom',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-left',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] } ]
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' initial overlapping keyframes (second in series of three)',
+    frames:   [ { margin: '5px', offset: 0 },
+                { margin: '5px', offset: 0,
+                  simulateComputeValuesFailure: true },
+                { margin: '5px', offset: 0 },
+                { margin: '5px' } ],
+    expected: [ { property: 'margin-top',
+                  values: [ value(0, '5px', 'replace'),
+                            value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-right',
+                  values: [ value(0, '5px', 'replace'),
+                            value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-bottom',
+                  values: [ value(0, '5px', 'replace'),
+                            value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-left',
+                  values: [ value(0, '5px', 'replace'),
+                            value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] } ]
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' final keyframe',
+    frames:   [ { margin: '5px' },
+                { margin: '5px', simulateComputeValuesFailure: true } ],
+    expected: [  ]
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' final keyframe where it forms the last segment in the series',
+    frames:   [ { margin: '5px' },
+                { margin: '5px',
+                  marginLeft: '5px',
+                  marginRight: '5px',
+                  marginBottom: '5px',
+                  // margin-top sorts last and only it will be missing since
+                  // the other longhand components are specified
+                  simulateComputeValuesFailure: true } ],
+    expected: [ { property: 'margin-bottom',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-left',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-right',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] } ]
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' final keyframe where we have enough values to create'
+              + ' an initial segment',
+    frames:   [ { margin: '5px' },
+                { margin: '5px' },
+                { margin: '5px', simulateComputeValuesFailure: true } ],
+    expected: [  ]
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' final overlapping keyframes (first in series of two)',
+    frames:   [ { margin: '5px' },
+                { margin: '5px', offset: 1,
+                  simulateComputeValuesFailure: true },
+                { margin: '5px', offset: 1 } ],
+    expected: [ { property: 'margin-top',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-right',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-bottom',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-left',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] } ]
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' final overlapping keyframes (second in series of two)',
+    frames:   [ { margin: '5px' },
+                { margin: '5px', offset: 1 },
+                { margin: '5px', offset: 1,
+                  simulateComputeValuesFailure: true } ],
+    expected: [ { property: 'margin-top',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-right',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-bottom',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-left',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] } ]
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' final overlapping keyframes (second in series of three)',
+    frames:   [ { margin: '5px' },
+                { margin: '5px', offset: 1 },
+                { margin: '5px', offset: 1,
+                  simulateComputeValuesFailure: true },
+                { margin: '5px', offset: 1 } ],
+    expected: [ { property: 'margin-top',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-right',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-bottom',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-left',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace'),
+                            value(1, '5px', 'replace') ] } ]
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' intermediate keyframe',
+    frames:   [ { margin: '5px' },
+                { margin: '5px', simulateComputeValuesFailure: true },
+                { margin: '5px' } ],
+    expected: [ { property: 'margin-top',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-right',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-bottom',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-left',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] } ]
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' initial keyframe along with other values',
+    // simulateComputeValuesFailure only applies to shorthands so we can set
+    // it on the same keyframe and it will only apply to |margin| and not
+    // |left|.
+    frames:   [ { margin: '77%', left: '10px',
+                  simulateComputeValuesFailure: true },
+                { margin: '5px', left: '20px' } ],
+    expected: [ { property: 'left',
+                  values: [ value(0, '10px', 'replace', 'linear'),
+                            value(1, '20px', 'replace') ] } ],
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' initial keyframe along with other values where those'
+              + ' values sort after the property with missing values',
+    frames:   [ { margin: '77%', right: '10px',
+                  simulateComputeValuesFailure: true },
+                { margin: '5px', right: '20px' } ],
+    expected: [ { property: 'right',
+                  values: [ value(0, '10px', 'replace', 'linear'),
+                            value(1, '20px', 'replace') ] } ],
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' final keyframe along with other values',
+    frames:   [ { margin: '5px', left: '10px' },
+                { margin: '5px', left: '20px',
+                  simulateComputeValuesFailure: true } ],
+    expected: [ { property: 'left',
+                  values: [ value(0, '10px', 'replace', 'linear'),
+                            value(1, '20px', 'replace') ] } ],
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' final keyframe along with other values where those'
+              + ' values sort after the property with missing values',
+    frames:   [ { margin: '5px', right: '10px' },
+                { margin: '5px', right: '20px',
+                  simulateComputeValuesFailure: true } ],
+    expected: [ { property: 'right',
+                  values: [ value(0, '10px', 'replace', 'linear'),
+                            value(1, '20px', 'replace') ] } ],
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' an intermediate keyframe along with other values',
+    frames:   [ { margin: '5px', left: '10px' },
+                { margin: '5px', left: '20px',
+                  simulateComputeValuesFailure: true },
+                { margin: '5px', left: '30px' } ],
+    expected: [ { property: 'margin-top',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-right',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-bottom',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-left',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'left',
+                  values: [ value(0,   '10px', 'replace', 'linear'),
+                            value(0.5, '20px', 'replace', 'linear'),
+                            value(1,   '30px', 'replace') ] } ]
+  },
+  { desc:     'a property that can\'t be resolved to computed values in'
+              + ' an intermediate keyframe by itself',
+    frames:   [ { margin: '5px', left: '10px' },
+                { margin: '5px',
+                  simulateComputeValuesFailure: true },
+                { margin: '5px', left: '30px' } ],
+    expected: [ { property: 'margin-top',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-right',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-bottom',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-left',
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'left',
+                  values: [ value(0,   '10px', 'replace', 'linear'),
+                            value(1,   '30px', 'replace') ] } ]
+  },
 ];
 
 gTests.forEach(function(subtest) {
   test(function(t) {
     var div = addDiv(t);
     var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
     assert_properties_equal(animation.effect.getProperties(),
                             subtest.expected);
--- a/dom/base/BlobSet.h
+++ b/dom/base/BlobSet.h
@@ -7,16 +7,17 @@
 #ifndef mozilla_dom_BlobSet_h
 #define mozilla_dom_BlobSet_h
 
 #include "mozilla/RefPtr.h"
 
 namespace mozilla {
 namespace dom {
 
+class Blob;
 class BlobImpl;
 
 class BlobSet final
 {
 public:
   BlobSet()
     : mData(nullptr)
     , mDataLen(0)
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -27,16 +27,19 @@
 #include "nsIScriptSecurityManager.h"
 #include "nsCharSeparatedTokenizer.h"
 #include "nsContentUtils.h"
 #include "nsUnicharUtils.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Telemetry.h"
 #include "BatteryManager.h"
 #include "mozilla/dom/DeviceStorageAreaListener.h"
+#ifdef MOZ_GAMEPAD
+#include "mozilla/dom/GamepadServiceTest.h"
+#endif
 #include "mozilla/dom/PowerManager.h"
 #include "mozilla/dom/WakeLock.h"
 #include "mozilla/dom/power/PowerManagerService.h"
 #include "mozilla/dom/CellBroadcast.h"
 #include "mozilla/dom/FlyWebPublishedServer.h"
 #include "mozilla/dom/FlyWebService.h"
 #include "mozilla/dom/IccManager.h"
 #include "mozilla/dom/InputPortManager.h"
@@ -260,16 +263,19 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mServiceWorkerContainer)
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
 #ifdef MOZ_EME
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaKeySystemAccessManager)
 #endif
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDeviceStorageAreaListener)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPresentation)
+#ifdef MOZ_GAMEPAD
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGamepadServiceTest)
+#endif
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVRGetDevicesPromises)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(Navigator)
 
 void
 Navigator::Invalidate()
@@ -402,16 +408,23 @@ Navigator::Invalidate()
     mMediaKeySystemAccessManager = nullptr;
   }
 #endif
 
   if (mDeviceStorageAreaListener) {
     mDeviceStorageAreaListener = nullptr;
   }
 
+#ifdef MOZ_GAMEPAD
+  if (mGamepadServiceTest) {
+    mGamepadServiceTest->Shutdown();
+    mGamepadServiceTest = nullptr;
+  }
+#endif
+
   mVRGetDevicesPromises.Clear();
 }
 
 //*****************************************************************************
 //    Navigator::nsIDOMNavigator
 //*****************************************************************************
 
 NS_IMETHODIMP
@@ -2015,16 +2028,25 @@ Navigator::GetGamepads(nsTArray<RefPtr<G
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return;
   }
   NS_ENSURE_TRUE_VOID(mWindow->GetDocShell());
   nsGlobalWindow* win = nsGlobalWindow::Cast(mWindow);
   win->SetHasGamepadEventListener(true);
   win->GetGamepads(aGamepads);
 }
+
+GamepadServiceTest*
+Navigator::RequestGamepadServiceTest()
+{
+  if (!mGamepadServiceTest) {
+    mGamepadServiceTest = GamepadServiceTest::CreateTestService(mWindow);
+  }
+  return mGamepadServiceTest;
+}
 #endif
 
 already_AddRefed<Promise>
 Navigator::GetVRDevices(ErrorResult& aRv)
 {
   if (!mWindow || !mWindow->GetDocShell()) {
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return nullptr;
--- a/dom/base/Navigator.h
+++ b/dom/base/Navigator.h
@@ -66,16 +66,17 @@ class FMRadio;
 
 class Promise;
 
 class DesktopNotificationCenter;
 class MobileMessageManager;
 class MozIdleObserver;
 #ifdef MOZ_GAMEPAD
 class Gamepad;
+class GamepadServiceTest;
 #endif // MOZ_GAMEPAD
 class NavigatorUserMediaSuccessCallback;
 class NavigatorUserMediaErrorCallback;
 class MozGetUserMediaDevicesSuccessCallback;
 
 namespace network {
 class Connection;
 } // namespace network
@@ -260,16 +261,17 @@ public:
   already_AddRefed<Promise> GetMobileIdAssertion(const MobileIdOptions& options,
                                                  ErrorResult& aRv);
 #endif
 #ifdef MOZ_B2G_RIL
   MobileConnectionArray* GetMozMobileConnections(ErrorResult& aRv);
 #endif // MOZ_B2G_RIL
 #ifdef MOZ_GAMEPAD
   void GetGamepads(nsTArray<RefPtr<Gamepad> >& aGamepads, ErrorResult& aRv);
+  GamepadServiceTest* RequestGamepadServiceTest();
 #endif // MOZ_GAMEPAD
   already_AddRefed<Promise> GetVRDevices(ErrorResult& aRv);
   void NotifyVRDevicesUpdated();
 #ifdef MOZ_B2G_FM
   FMRadio* GetMozFMRadio(ErrorResult& aRv);
 #endif
 #ifdef MOZ_B2G_BT
   bluetooth::BluetoothManager* GetMozBluetooth(ErrorResult& aRv);
@@ -393,17 +395,19 @@ private:
   RefPtr<MediaDevices> mMediaDevices;
   nsCOMPtr<nsIDOMNavigatorSystemMessages> mMessagesManager;
   nsTArray<nsWeakPtr> mDeviceStorageStores;
   RefPtr<time::TimeManager> mTimeManager;
   RefPtr<ServiceWorkerContainer> mServiceWorkerContainer;
   nsCOMPtr<nsPIDOMWindowInner> mWindow;
   RefPtr<DeviceStorageAreaListener> mDeviceStorageAreaListener;
   RefPtr<Presentation> mPresentation;
-
+#ifdef MOZ_GAMEPAD
+  RefPtr<GamepadServiceTest> mGamepadServiceTest;
+#endif
   nsTArray<RefPtr<Promise> > mVRGetDevicesPromises;
   nsTArray<uint32_t> mRequestedVibrationPattern;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_Navigator_h
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -30,17 +30,16 @@ XPIDL_SOURCES += [
     'nsISelection.idl',
     'nsISelectionController.idl',
     'nsISelectionDisplay.idl',
     'nsISelectionListener.idl',
     'nsISelectionPrivate.idl',
     'nsISimpleContentPolicy.idl',
     'nsISiteSpecificUserAgent.idl',
     'nsISlowScriptDebug.idl',
-    'nsIXMLHttpRequest.idl',
 ]
 
 XPIDL_MODULE = 'dom'
 
 EXPORTS += [
     'AutocompleteFieldList.h',
     'Crypto.h',
     'HTMLSplitOnSpacesTokenizer.h',
@@ -317,17 +316,16 @@ UNIFIED_SOURCES += [
     'nsTraversal.cpp',
     'nsTreeSanitizer.cpp',
     'nsViewportInfo.cpp',
     'nsWindowMemoryReporter.cpp',
     'nsWindowRoot.cpp',
     'nsWrapperCache.cpp',
     'nsXHTMLContentSerializer.cpp',
     'nsXMLContentSerializer.cpp',
-    'nsXMLHttpRequest.cpp',
     'nsXMLNameSpaceMap.cpp',
     'PostMessageEvent.cpp',
     'ProcessGlobal.cpp',
     'ResponsiveImageSelector.cpp',
     'SameProcessMessageQueue.cpp',
     'ScreenOrientation.cpp',
     'ScriptSettings.cpp',
     'ShadowRoot.cpp',
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -2293,17 +2293,17 @@ nsDOMWindowUtils::GetSupportsHardwareH26
   NS_ENSURE_STATE(promise);
   aPromise.setObject(*promise->PromiseObj());
 #else
   ErrorResult rv;
   RefPtr<Promise> promise = Promise::Create(parentObject, rv);
   if (rv.Failed()) {
     return rv.StealNSResult();
   }
-  promise.MaybeResolve(NS_LITERAL_STRING("No; Compiled without MP4 support."));
+  promise->MaybeResolve(NS_LITERAL_STRING("No; Compiled without MP4 support."));
   aPromise.setObject(*promise->PromiseObj());
 #endif
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDOMWindowUtils::StartFrameTimeRecording(uint32_t *startIndex)
 {
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -184,17 +184,17 @@
 #include "prprf.h"
 
 #include "mozilla/dom/IDBFactory.h"
 #include "mozilla/dom/MessageChannel.h"
 #include "mozilla/dom/Promise.h"
 
 #ifdef MOZ_GAMEPAD
 #include "mozilla/dom/Gamepad.h"
-#include "mozilla/dom/GamepadService.h"
+#include "mozilla/dom/GamepadManager.h"
 #endif
 
 #include "mozilla/dom/VRDevice.h"
 
 #include "nsRefreshDriver.h"
 #include "Layers.h"
 
 #include "mozilla/AddonPathService.h"
@@ -9914,34 +9914,34 @@ void nsGlobalWindow::MaybeUpdateTouchSta
 
 void
 nsGlobalWindow::EnableGamepadUpdates()
 {
   MOZ_ASSERT(IsInnerWindow());
 
   if (mHasGamepad) {
 #ifdef MOZ_GAMEPAD
-    RefPtr<GamepadService> gamepadsvc(GamepadService::GetService());
-    if (gamepadsvc) {
-      gamepadsvc->AddListener(this);
+    RefPtr<GamepadManager> gamepadManager(GamepadManager::GetService());
+    if (gamepadManager) {
+      gamepadManager->AddListener(this);
     }
 #endif
   }
 }
 
 void
 nsGlobalWindow::DisableGamepadUpdates()
 {
   MOZ_ASSERT(IsInnerWindow());
 
   if (mHasGamepad) {
 #ifdef MOZ_GAMEPAD
-    RefPtr<GamepadService> gamepadsvc(GamepadService::GetService());
-    if (gamepadsvc) {
-      gamepadsvc->RemoveListener(this);
+    RefPtr<GamepadManager> gamepadManager(GamepadManager::GetService());
+    if (gamepadManager) {
+      gamepadManager->RemoveListener(this);
     }
 #endif
   }
 }
 
 void
 nsGlobalWindow::SetChromeEventHandler(EventTarget* aChromeEventHandler)
 {
@@ -13398,19 +13398,19 @@ nsGlobalWindow::HasSeenGamepadInput()
   return mHasSeenGamepadInput;
 }
 
 void
 nsGlobalWindow::SyncGamepadState()
 {
   MOZ_ASSERT(IsInnerWindow());
   if (mHasSeenGamepadInput) {
-    RefPtr<GamepadService> gamepadsvc(GamepadService::GetService());
+    RefPtr<GamepadManager> gamepadManager(GamepadManager::GetService());
     for (auto iter = mGamepads.Iter(); !iter.Done(); iter.Next()) {
-      gamepadsvc->SyncGamepadState(iter.Key(), iter.UserData());
+      gamepadManager->SyncGamepadState(iter.Key(), iter.UserData());
     }
   }
 }
 #endif // MOZ_GAMEPAD
 
 bool
 nsGlobalWindow::UpdateVRDevices(nsTArray<RefPtr<mozilla::dom::VRDevice>>& aDevices)
 {
--- a/dom/base/nsObjectLoadingContent.cpp
+++ b/dom/base/nsObjectLoadingContent.cpp
@@ -2259,17 +2259,20 @@ nsObjectLoadingContent::LoadObject(bool 
 
   if (mType != eType_Null) {
     bool allowLoad = true;
     if (IsJavaMIME(mContentType)) {
       allowLoad = CheckJavaCodebase();
     }
     int16_t contentPolicy = nsIContentPolicy::ACCEPT;
     // If mChannelLoaded is set we presumably already passed load policy
-    if (allowLoad && mURI && !mChannelLoaded) {
+    // If mType == eType_Loading then we call OpenChannel() which internally
+    // creates a new channel and calls asyncOpen2() on that channel which
+    // then enforces content policy checks.
+    if (allowLoad && mURI && !mChannelLoaded && mType != eType_Loading) {
       allowLoad = CheckLoadPolicy(&contentPolicy);
     }
     // If we're loading a type now, check ProcessPolicy. Note that we may check
     // both now in the case of plugins whose type is determined before opening a
     // channel.
     if (allowLoad && mType != eType_Loading) {
       allowLoad = CheckProcessPolicy(&contentPolicy);
     }
@@ -2612,44 +2615,39 @@ nsObjectLoadingContent::CloseChannel()
   return NS_OK;
 }
 
 nsresult
 nsObjectLoadingContent::OpenChannel()
 {
   nsCOMPtr<nsIContent> thisContent =
     do_QueryInterface(static_cast<nsIImageLoadingContent*>(this));
-  nsCOMPtr<nsIScriptSecurityManager> secMan =
-    nsContentUtils::GetSecurityManager();
   NS_ASSERTION(thisContent, "must be a content");
   nsIDocument* doc = thisContent->OwnerDoc();
   NS_ASSERTION(doc, "No owner document?");
 
   nsresult rv;
   mChannel = nullptr;
 
   // E.g. mms://
   if (!mURI || !CanHandleURI(mURI)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
-  rv = secMan->CheckLoadURIWithPrincipal(thisContent->NodePrincipal(), mURI, 0);
-  NS_ENSURE_SUCCESS(rv, rv);
-
   nsCOMPtr<nsILoadGroup> group = doc->GetDocumentLoadGroup();
   nsCOMPtr<nsIChannel> chan;
   RefPtr<ObjectInterfaceRequestorShim> shim =
     new ObjectInterfaceRequestorShim(this);
 
   bool isSandBoxed = doc->GetSandboxFlags() & SANDBOXED_ORIGIN;
   bool inherit = nsContentUtils::ChannelShouldInheritPrincipal(thisContent->NodePrincipal(),
                                                                mURI,
                                                                true,   // aInheritForAboutBlank
                                                                false); // aForceInherit
-  nsSecurityFlags securityFlags = nsILoadInfo::SEC_NORMAL;
+  nsSecurityFlags securityFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS;
   if (inherit) {
     securityFlags |= nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL;
   }
   if (isSandBoxed) {
     securityFlags |= nsILoadInfo::SEC_SANDBOXED;
   }
 
   nsContentPolicyType contentPolicyType = GetContentPolicyType();
@@ -2681,18 +2679,18 @@ nsObjectLoadingContent::OpenChannel()
   }
 
   nsCOMPtr<nsIScriptChannel> scriptChannel = do_QueryInterface(chan);
   if (scriptChannel) {
     // Allow execution against our context if the principals match
     scriptChannel->SetExecutionPolicy(nsIScriptChannel::EXECUTE_NORMAL);
   }
 
-  // AsyncOpen can fail if a file does not exist.
-  rv = chan->AsyncOpen(shim, nullptr);
+  // AsyncOpen2 can fail if a file does not exist.
+  rv = chan->AsyncOpen2(shim);
   NS_ENSURE_SUCCESS(rv, rv);
   LOG(("OBJLC [%p]: Channel opened", this));
   mChannel = chan;
   return NS_OK;
 }
 
 uint32_t
 nsObjectLoadingContent::GetCapabilities() const
--- a/dom/base/test/fileutils.js
+++ b/dom/base/test/fileutils.js
@@ -44,17 +44,17 @@ function testFile(file, contents, test) 
   var fd = new FormData;
   fd.append("hello", "world");
   fd.append("myfile", file);
   xhr.send(fd);
   expectedTestCount++;
 
   // Send file to server using plain XMLHttpRequest
   var xhr = new XMLHttpRequest;
-  xhr.open("POST", "file_XHRSendData.sjs");
+  xhr.open("POST", "../../../dom/xhr/tests/file_XHRSendData.sjs");
   xhr.onload = function (event) {
     is(event.target.getResponseHeader("Result-Content-Type"),
        file.type ? file.type : null,
        "request content-type in XMLHttpRequest send of " + test);
     is(event.target.getResponseHeader("Result-Content-Length"),
        String(file.size),
        "request content-length in XMLHttpRequest send of " + test);
   };
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -53,52 +53,22 @@ support-files =
   bug696301-script-2.js
   bug704320.sjs
   bug704320_counter.sjs
   bug819051.sjs
   chrome/bug418986-1.js
   copypaste.js
   create_file_objects.js
   delayedServerEvents.sjs
-  echo.sjs
   eventsource.resource
   eventsource.resource^headers^
   eventsource_redirect.resource
   eventsource_redirect.resource^headers^
   eventsource_redirect_to.resource
   eventsource_redirect_to.resource^headers^
-  file_XHRDocURI.text
-  file_XHRDocURI.text^headers^
-  file_XHRDocURI.xml
-  file_XHRDocURI.xml^headers^
-  file_XHRDocURI.html
-  file_XHRDocURI.html^headers^
-  file_XHRDocURI.sjs
-  file_XHRResponseURL.js
-  file_XHRResponseURL.sjs
-  file_XHRResponseURL.text
-  file_XHRResponseURL.text^headers^
-  file_XHRResponseURL_nocors.text
-  file_XHRSendData.sjs
-  file_XHRSendData_doc.xml
-  file_XHRSendData_doc.xml^headers^
-  file_XHR_anon.sjs
-  file_XHR_binary1.bin
-  file_XHR_binary1.bin^headers^
-  file_XHR_binary2.bin
-  file_XHR_fail1.txt
-  file_XHR_fail1.txt^headers^
-  file_XHR_header.sjs
-  file_XHR_pass1.xml
-  file_XHR_pass2.txt
-  file_XHR_pass3.txt
-  file_XHR_pass3.txt^headers^
-  file_XHR_system_redirect.html
-  file_XHR_system_redirect.html^headers^
-  file_XHR_timeout.sjs
   file_base_xbl.xml
   file_bug1091883_frame.html
   file_bug1091883_subframe.html
   file_bug1091883_target.html
   file_bug28293.sjs
   file_bug326337.xml
   file_bug326337_inner.html
   file_bug326337_outer.html
@@ -157,20 +127,16 @@ support-files =
   file_bug902350.html
   file_bug902350_frame.html
   file_bug907892.html
   file_bug945152.jar
   file_bug1263696_frame_pass.html
   file_bug1263696_frame_fail.html
   file_bug1274806.html
   file_general_document.html
-  file_html_in_xhr.html
-  file_html_in_xhr.sjs
-  file_html_in_xhr2.html
-  file_html_in_xhr3.html
   file_htmlserializer_1.html
   file_htmlserializer_1_bodyonly.html
   file_htmlserializer_1_format.html
   file_htmlserializer_1_linebreak.html
   file_htmlserializer_1_links.html
   file_htmlserializer_1_nested_body.html
   file_htmlserializer_1_no_body.html
   file_htmlserializer_1_noflag.html
@@ -227,18 +193,16 @@ support-files =
   fileutils.js
   forRemoval.resource
   forRemoval.resource^headers^
   formReset.html
   invalid_accesscontrol.resource
   invalid_accesscontrol.resource^headers^
   mutationobserver_dialog.html
   orientationcommon.js
-  progressserver.sjs
-  responseIdentical.sjs
   script-1_bug597345.sjs
   script-2_bug597345.js
   script_bug602838.sjs
   send_gzip_content.sjs
   somedatas.resource
   somedatas.resource^headers^
   variable_style_sheet.sjs
   viewport_helpers.js
@@ -264,27 +228,26 @@ support-files =
   file_bug1198095.js
   file_bug1250148.sjs
   mozbrowser_api_utils.js
   websocket_helpers.js
   websocket_tests.js
   !/dom/html/test/form_submit_server.sjs
   !/dom/security/test/cors/file_CrossSiteXHR_server.sjs
   !/image/test/mochitest/blue.png
+  !/dom/xhr/tests/file_XHRSendData.sjs
   script_bug1238440.js
 
 [test_anchor_area_referrer.html]
 [test_anchor_area_referrer_changing.html]
 [test_anchor_area_referrer_invalid.html]
 [test_anchor_area_referrer_rel.html]
 [test_anonymousContent_api.html]
 [test_anonymousContent_append_after_reflow.html]
 [test_anonymousContent_canvas.html]
-[test_xhr_overridemimetype_throws_on_invalid_state.html]
-skip-if = buildapp == 'b2g' # Requires webgl support
 [test_anonymousContent_insert.html]
 [test_anonymousContent_manipulate_content.html]
 [test_anonymousContent_style_csp.html]
 [test_applet_alternate_content.html]
 [test_appname_override.html]
 [test_async_setTimeout_stack.html]
 [test_async_setTimeout_stack_across_globals.html]
 [test_audioWindowUtils.html]
@@ -721,17 +684,16 @@ skip-if = e10s || os != 'linux' || build
 [test_gsp-qualified.html]
 [test_gsp-quirks.html]
 [test_gsp-standards.html]
 [test_hasFeature.html]
 [test_history_document_open.html]
 [test_history_state_null.html]
 [test_html_colors_quirks.html]
 [test_html_colors_standards.html]
-[test_html_in_xhr.html]
 [test_htmlcopyencoder.html]
 [test_htmlcopyencoder.xhtml]
 [test_iframe_referrer.html]
 [test_iframe_referrer_changing.html]
 [test_iframe_referrer_invalid.html]
 [test_Image_constructor.html]
 [test_img_referrer.html]
 [test_innersize_scrollport.html]
@@ -822,18 +784,16 @@ skip-if = toolkit == 'android'
 [test_setInterval_uncatchable_exception.html]
 skip-if = debug == false
 [test_settimeout_extra_arguments.html]
 [test_settimeout_inner.html]
 [test_setting_opener.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') #Bug 931116, b2g desktop specific, initial triage
 [test_simplecontentpolicy.html]
 skip-if = e10s || buildapp == 'b2g' # Bug 1156489.
-[test_sync_xhr_timer.xhtml]
-skip-if = toolkit == 'android'
 [test_text_wholeText.html]
 [test_textnode_normalize_in_selection.html]
 [test_textnode_split_in_selection.html]
 [test_title.html]
 [test_treewalker_nextsibling.xml]
 [test_unknown_url_origin.html]
 [test_url.html]
 [test_url_data.html]
@@ -884,32 +844,10 @@ skip-if = release_build
 [test_window_indexing.html]
 [test_window_named_frame_enumeration.html]
 [test_window_orientation.html]
 skip-if = toolkit != 'gonk'
 [test_window_proto.html]
 [test_writable-replaceable.html]
 [test_x-frame-options.html]
 [test_xbl_userdata.xhtml]
-[test_XHR.html]
-[test_xhr_abort_after_load.html]
-skip-if = toolkit == 'android'
-[test_XHR_anon.html]
-[test_xhr_forbidden_headers.html]
-[test_XHR_header.html]
-[test_XHR_onuploadprogress.html]
-[test_XHR_parameters.html]
-skip-if = buildapp == 'b2g' # b2g(86 total, 4 failing - testing mozAnon - got false, expected true) b2g-debug(86 total, 4 failing - testing mozAnon - got false, expected true) b2g-desktop(86 total, 4 failing - testing mozAnon - got false, expected true)
-[test_xhr_progressevents.html]
-skip-if = toolkit == 'android'
-[test_xhr_send.html]
-[test_xhr_send_readystate.html]
-[test_XHR_system.html]
-skip-if = (buildapp == 'b2g' && (toolkit != 'gonk' || debug)) # b2g-debug(12 total, 2 failing - .mozSystem == true - got false, expected true + ) b2g-desktop(12 total, 2 failing - .mozSystem == true - got false, expected true + )
-[test_XHR_timeout.html]
-skip-if = buildapp == 'b2g' || (android_version == '18' && debug) # b2g(flaky on B2G, bug 960743) b2g-debug(flaky on B2G, bug 960743) b2g-desktop(flaky on B2G, bug 960743)
-support-files = test_XHR_timeout.js
-[test_xhr_withCredentials.html]
-[test_XHRDocURI.html]
-[test_XHRResponseURL.html]
-[test_XHRSendData.html]
 [test_youtube_flash_embed.html]
 # Please keep alphabetical order.
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -1572,40 +1572,24 @@ DOMInterfaces = {
     ],
     # Rename a few things so we don't have both classes and methods
     # with the same name
     'binaryNames': {
         'performance': 'getPerformance',
     },
 },
 
-'XMLHttpRequest': [
-{
-    'nativeType': 'nsXMLHttpRequest',
+'XMLHttpRequest': {
     'implicitJSContext': [ 'send'],
 },
-{
-    'workers': True,
-}],
 
 'XMLHttpRequestEventTarget': {
-    'nativeType': 'nsXHREventTarget',
-    'headerFile': 'nsXMLHttpRequest.h',
     'concrete': False
 },
 
-'XMLHttpRequestUpload': [
-{
-    'nativeType': 'nsXMLHttpRequestUpload',
-    'headerFile': 'nsXMLHttpRequest.h'
-},
-{
-    'workers': True,
-}],
-
 'XMLSerializer': {
     'nativeType': 'nsDOMSerializer',
 },
 
 'XPathEvaluator': {
     'wrapperCache': False
 },
 
--- a/dom/bindings/Errors.msg
+++ b/dom/bindings/Errors.msg
@@ -90,12 +90,13 @@ MSG_DEF(MSG_PROMISE_ARG_NOT_ITERABLE, 1,
 MSG_DEF(MSG_IS_NOT_PROMISE, 1, JSEXN_TYPEERR, "{0} is not a Promise")
 MSG_DEF(MSG_SW_INSTALL_ERROR, 2, JSEXN_TYPEERR, "ServiceWorker script at {0} for scope {1} encountered an error during installation.")
 MSG_DEF(MSG_SW_SCRIPT_THREW, 2, JSEXN_TYPEERR, "ServiceWorker script at {0} for scope {1} threw an exception during script evaluation.")
 MSG_DEF(MSG_TYPEDARRAY_IS_SHARED, 1, JSEXN_TYPEERR, "{0} can't be a typed array on SharedArrayBuffer")
 MSG_DEF(MSG_CACHE_ADD_FAILED_RESPONSE, 3, JSEXN_TYPEERR, "Cache got {0} response with bad status {1} while trying to add request {2}")
 MSG_DEF(MSG_SW_UPDATE_BAD_REGISTRATION, 2, JSEXN_TYPEERR, "Failed to update the ServiceWorker for scope {0] because the registration has been {1} since the update was scheduled.")
 MSG_DEF(MSG_INVALID_DURATION_ERROR, 1, JSEXN_TYPEERR, "Invalid duration '{0}'.")
 MSG_DEF(MSG_INVALID_EASING_ERROR, 1, JSEXN_TYPEERR, "Invalid easing '{0}'.")
+MSG_DEF(MSG_INVALID_SPACING_MODE_ERROR, 1, JSEXN_TYPEERR, "Invalid spacing '{0}'.")
 MSG_DEF(MSG_USELESS_SETTIMEOUT, 1, JSEXN_TYPEERR, "Useless {0} call (missing quotes around argument?)")
 MSG_DEF(MSG_TOKENLIST_NO_SUPPORTED_TOKENS, 2, JSEXN_TYPEERR, "{0} attribute of <{1}> does not define any supported tokens")
 MSG_DEF(MSG_CACHE_STREAM_CLOSED, 0, JSEXN_TYPEERR, "Response body is a cache file stream that has already been closed.")
 MSG_DEF(MSG_ONLY_IF_CACHED_WITHOUT_SAME_ORIGIN, 1, JSEXN_TYPEERR, "Request mode '{0}' was used, but request cache mode 'only-if-cached' can only be used with request mode 'same-origin'.")
--- a/dom/bluetooth/common/webapi/BluetoothPbapRequestHandle.h
+++ b/dom/bluetooth/common/webapi/BluetoothPbapRequestHandle.h
@@ -5,17 +5,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_bluetooth_bluetoothpbaprequesthandle_h
 #define mozilla_dom_bluetooth_bluetoothpbaprequesthandle_h
 
 #include "nsCOMPtr.h"
 #include "mozilla/dom/bluetooth/BluetoothCommon.h"
 #include "mozilla/dom/DOMRequest.h"
-#include "mozilla/dom/BlobSet.h"
 
 namespace mozilla {
   class ErrorResult;
   namespace dom {
     class Blob;
     class DOMRequest;
   }
 }
--- a/dom/canvas/WebGLContext.cpp
+++ b/dom/canvas/WebGLContext.cpp
@@ -62,16 +62,17 @@
 #include "WebGLExtensions.h"
 #include "WebGLFramebuffer.h"
 #include "WebGLMemoryTracker.h"
 #include "WebGLObjectModel.h"
 #include "WebGLProgram.h"
 #include "WebGLQuery.h"
 #include "WebGLSampler.h"
 #include "WebGLShader.h"
+#include "WebGLSync.h"
 #include "WebGLTimerQuery.h"
 #include "WebGLTransformFeedback.h"
 #include "WebGLVertexArray.h"
 #include "WebGLVertexAttribData.h"
 
 #ifdef MOZ_WIDGET_COCOA
 #include "nsCocoaFeatures.h"
 #endif
@@ -261,16 +262,17 @@ WebGLContext::DestroyResourcesAndContext
 
     ClearLinkedList(mBuffers);
     ClearLinkedList(mFramebuffers);
     ClearLinkedList(mPrograms);
     ClearLinkedList(mQueries);
     ClearLinkedList(mRenderbuffers);
     ClearLinkedList(mSamplers);
     ClearLinkedList(mShaders);
+    ClearLinkedList(mSyncs);
     ClearLinkedList(mTextures);
     ClearLinkedList(mTimerQueries);
     ClearLinkedList(mTransformFeedbacks);
     ClearLinkedList(mVertexArrays);
 
     //////
 
     mFakeBlack_2D_0000       = nullptr;
--- a/dom/canvas/WebGLContext.h
+++ b/dom/canvas/WebGLContext.h
@@ -95,16 +95,17 @@ class WebGLBuffer;
 class WebGLExtensionBase;
 class WebGLFramebuffer;
 class WebGLProgram;
 class WebGLQuery;
 class WebGLRenderbuffer;
 class WebGLSampler;
 class WebGLShader;
 class WebGLShaderPrecisionFormat;
+class WebGLSync;
 class WebGLTexture;
 class WebGLTimerQuery;
 class WebGLTransformFeedback;
 class WebGLUniformLocation;
 class WebGLVertexArray;
 
 namespace dom {
 class Element;
@@ -1397,16 +1398,17 @@ protected:
 
     LinkedList<WebGLBuffer> mBuffers;
     LinkedList<WebGLFramebuffer> mFramebuffers;
     LinkedList<WebGLProgram> mPrograms;
     LinkedList<WebGLQuery> mQueries;
     LinkedList<WebGLRenderbuffer> mRenderbuffers;
     LinkedList<WebGLSampler> mSamplers;
     LinkedList<WebGLShader> mShaders;
+    LinkedList<WebGLSync> mSyncs;
     LinkedList<WebGLTexture> mTextures;
     LinkedList<WebGLTimerQuery> mTimerQueries;
     LinkedList<WebGLTransformFeedback> mTransformFeedbacks;
     LinkedList<WebGLVertexArray> mVertexArrays;
 
     WebGLRefPtr<WebGLTransformFeedback> mDefaultTransformFeedback;
     WebGLRefPtr<WebGLVertexArray> mDefaultVertexArray;
 
--- a/dom/canvas/WebGLContextDraw.cpp
+++ b/dom/canvas/WebGLContextDraw.cpp
@@ -839,16 +839,32 @@ WebGLContext::FakeBlackTexture::FakeBlac
     // minimize the risk of running into a driver bug in texImage2D, as it is a bit
     // unusual maybe to create 1x1 textures, and the stack may not have the alignment that
     // TexImage2D expects.
 
     const webgl::DriverUnpackInfo dui = {texFormat, texFormat, LOCAL_GL_UNSIGNED_BYTE};
     UniqueBuffer zeros = moz_xcalloc(1, 16); // Infallible allocation.
 
     MOZ_ASSERT(gl->IsCurrent());
+    auto logANGLEError = [](GLenum source, GLenum type, GLuint id, GLenum severity,
+                            GLsizei length, const GLchar* message, const GLvoid* userParam)
+    {
+        gfxCriticalNote << message;
+    };
+
+    if (gl->IsANGLE()) {
+      gl->fEnable(LOCAL_GL_DEBUG_OUTPUT);
+      gl->fDebugMessageCallback(logANGLEError, nullptr);
+      gl->fDebugMessageControl(LOCAL_GL_DONT_CARE,
+                               LOCAL_GL_DONT_CARE,
+                               LOCAL_GL_DONT_CARE,
+                               0, nullptr,
+                               true);
+    }
+
     if (target == LOCAL_GL_TEXTURE_CUBE_MAP) {
         for (int i = 0; i < 6; ++i) {
             const TexImageTarget curTarget = LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X + i;
             const GLenum error = DoTexImage(mGL, curTarget.get(), 0, &dui, 1, 1, 1,
                                             zeros.get());
             if (error) {
                 const nsPrintfCString text("DoTexImage failed with `error`: 0x%04x, "
                                            "for `curTarget`: 0x%04x, "
@@ -867,16 +883,20 @@ WebGLContext::FakeBlackTexture::FakeBlac
                                        "for `target`: 0x%04x, "
                                        "`dui`: {0x%04x, 0x%04x, 0x%04x}.",
                                        error, target.get(), dui.internalFormat,
                                        dui.unpackFormat, dui.unpackType);
             gfxCriticalError() << text.BeginReading();
             MOZ_CRASH("GFX: Unexpected error during FakeBlack creation.");
         }
     }
+
+    if (gl->IsANGLE()) {
+      gl->fDisable(LOCAL_GL_DEBUG_OUTPUT);
+    }
 }
 
 WebGLContext::FakeBlackTexture::~FakeBlackTexture()
 {
     mGL->MakeCurrent();
     mGL->fDeleteTextures(1, &mGLName);
 }
 
--- a/dom/canvas/WebGLSync.cpp
+++ b/dom/canvas/WebGLSync.cpp
@@ -9,31 +9,32 @@
 #include "mozilla/dom/WebGL2RenderingContextBinding.h"
 #include "WebGLContext.h"
 
 namespace mozilla {
 
 WebGLSync::WebGLSync(WebGLContext* webgl, GLenum condition, GLbitfield flags)
     : WebGLContextBoundObject(webgl)
 {
+   mContext->mSyncs.insertBack(this);
    mGLName = mContext->gl->fFenceSync(condition, flags);
 }
 
 WebGLSync::~WebGLSync()
 {
     DeleteOnce();
 }
 
 void
 WebGLSync::Delete()
 {
     mContext->MakeContextCurrent();
     mContext->gl->fDeleteSync(mGLName);
     mGLName = 0;
-    LinkedListElement<WebGLSync>::remove();
+    LinkedListElement<WebGLSync>::removeFrom(mContext->mSyncs);
 }
 
 WebGLContext*
 WebGLSync::GetParentObject() const
 {
     return mContext;
 }
 
deleted file mode 100644
--- a/dom/gamepad/GamepadFunctions.cpp
+++ /dev/null
@@ -1,106 +0,0 @@
-/* -*- 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 "mozilla/dom/GamepadFunctions.h"
-#include "mozilla/dom/GamepadService.h"
-#include "nsHashKeys.h"
-#include "mozilla/dom/ContentParent.h"
-#include "mozilla/unused.h"
-
-namespace mozilla {
-namespace dom {
-namespace GamepadFunctions {
-
-namespace {
-// Increments as gamepads are added
-uint32_t gGamepadIndex = 0;
-}
-
-template<class T>
-void
-NotifyGamepadChange(const T& aInfo)
-{
-  MOZ_ASSERT(NS_IsMainThread());
-  MOZ_ASSERT(XRE_IsParentProcess());
-  GamepadChangeEvent e(aInfo);
-  nsTArray<ContentParent*> t;
-  ContentParent::GetAll(t);
-  for(uint32_t i = 0; i < t.Length(); ++i) {
-    Unused << t[i]->SendGamepadUpdate(e);
-  }
-  // If we have a GamepadService in the main process, send directly to it.
-  if (GamepadService::IsServiceRunning()) {
-    RefPtr<GamepadService> svc = GamepadService::GetService();
-    svc->Update(e);
-  }
-}
-
-uint32_t
-AddGamepad(const char* aID,
-           GamepadMappingType aMapping,
-           uint32_t aNumButtons, uint32_t aNumAxes)
-{
-  MOZ_ASSERT(NS_IsMainThread());
-  MOZ_ASSERT(XRE_IsParentProcess());
-
-  int index = gGamepadIndex;
-  gGamepadIndex++;
-  GamepadAdded a(NS_ConvertUTF8toUTF16(nsDependentCString(aID)), index,
-                 (uint32_t)aMapping, aNumButtons, aNumAxes);
-  NotifyGamepadChange<GamepadAdded>(a);
-  return index;
-}
-
-void
-RemoveGamepad(uint32_t aIndex)
-{
-  MOZ_ASSERT(NS_IsMainThread());
-  MOZ_ASSERT(XRE_IsParentProcess());
-  GamepadRemoved a(aIndex);
-  NotifyGamepadChange<GamepadRemoved>(a);
-}
-
-void
-NewButtonEvent(uint32_t aIndex, uint32_t aButton,
-               bool aPressed, double aValue)
-{
-  MOZ_ASSERT(NS_IsMainThread());
-  MOZ_ASSERT(XRE_IsParentProcess());
-  GamepadButtonInformation a(aIndex, aButton, aPressed, aValue);
-  NotifyGamepadChange<GamepadButtonInformation>(a);
-}
-
-void
-NewButtonEvent(uint32_t aIndex, uint32_t aButton,
-               bool aPressed)
-{
-  MOZ_ASSERT(NS_IsMainThread());
-  MOZ_ASSERT(XRE_IsParentProcess());
-  // When only a digital button is available the value will be synthesized.
-  NewButtonEvent(aIndex, aButton, aPressed, aPressed ? 1.0L : 0.0L);
-}
-
-void
-NewAxisMoveEvent(uint32_t aIndex, uint32_t aAxis,
-                 double aValue)
-{
-  MOZ_ASSERT(NS_IsMainThread());
-  MOZ_ASSERT(XRE_IsParentProcess());
-  GamepadAxisInformation a(aIndex, aAxis, aValue);
-  NotifyGamepadChange<GamepadAxisInformation>(a);
-}
-
-void
-ResetGamepadIndexes()
-{
-  MOZ_ASSERT(NS_IsMainThread());
-  MOZ_ASSERT(XRE_IsParentProcess());
-  gGamepadIndex = 0;
-}
-
-} // namespace GamepadFunctions
-} // namespace dom
-} // namespace mozilla
deleted file mode 100644
--- a/dom/gamepad/GamepadFunctions.h
+++ /dev/null
@@ -1,47 +0,0 @@
-/* -*- 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/. */
-
-#ifndef mozilla_dom_GamepadFunctions_h_
-#define mozilla_dom_GamepadFunctions_h_
-
-#include "mozilla/dom/GamepadBinding.h"
-
-namespace mozilla {
-namespace dom {
-namespace GamepadFunctions {
-
-// Functions for building and transmitting IPDL messages through the HAL
-// sandbox. Used by platform specific Gamepad implementations
-
-// Add a gamepad to the list of known gamepads, and return its index.
-uint32_t AddGamepad(const char* aID, GamepadMappingType aMapping,
-                    uint32_t aNumButtons, uint32_t aNumAxes);
-// Remove the gamepad at |aIndex| from the list of known gamepads.
-void RemoveGamepad(uint32_t aIndex);
-
-// Update the state of |aButton| for the gamepad at |aIndex| for all
-// windows that are listening and visible, and fire one of
-// a gamepadbutton{up,down} event at them as well.
-// aPressed is used for digital buttons, aValue is for analog buttons.
-void NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed,
-                    double aValue);
-// When only a digital button is available the value will be synthesized.
-void NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed);
-
-// Update the state of |aAxis| for the gamepad at |aIndex| for all
-// windows that are listening and visible, and fire a gamepadaxismove
-// event at them as well.
-void NewAxisMoveEvent(uint32_t aIndex, uint32_t aAxis, double aValue);
-
-// When shutting down the platform communications for gamepad, also reset the
-// indexes.
-void ResetGamepadIndexes();
-
-} // namespace GamepadFunctions
-} // namespace dom
-} // namespace mozilla
-
-#endif
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/GamepadManager.cpp
@@ -0,0 +1,596 @@
+/* -*- 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 "mozilla/dom/GamepadManager.h"
+
+#include "mozilla/dom/Gamepad.h"
+#include "mozilla/dom/GamepadAxisMoveEvent.h"
+#include "mozilla/dom/GamepadButtonEvent.h"
+#include "mozilla/dom/GamepadEvent.h"
+#include "mozilla/dom/GamepadEventChannelChild.h"
+#include "mozilla/dom/GamepadMonitoring.h"
+
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPtr.h"
+
+#include "nsAutoPtr.h"
+#include "nsGlobalWindow.h"
+#include "nsIDOMEvent.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMWindow.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsIServiceManager.h"
+#include "nsThreadUtils.h"
+#include "mozilla/Services.h"
+#include "mozilla/unused.h"
+
+#include <cstddef>
+
+using namespace mozilla::ipc;
+
+namespace mozilla {
+namespace dom {
+
+namespace {
+
+const char* kGamepadEnabledPref = "dom.gamepad.enabled";
+const char* kGamepadEventsEnabledPref =
+  "dom.gamepad.non_standard_events.enabled";
+
+const nsTArray<RefPtr<nsGlobalWindow>>::index_type NoIndex =
+  nsTArray<RefPtr<nsGlobalWindow>>::NoIndex;
+
+bool sShutdown = false;
+
+StaticRefPtr<GamepadManager> gGamepadManagerSingleton;
+
+} // namespace
+
+NS_IMPL_ISUPPORTS(GamepadManager, nsIObserver)
+
+GamepadManager::GamepadManager()
+  : mEnabled(false),
+    mNonstandardEventsEnabled(false),
+    mShuttingDown(false),
+    mChild(nullptr)
+{}
+
+nsresult
+GamepadManager::Init()
+{
+  mEnabled = IsAPIEnabled();
+  mNonstandardEventsEnabled =
+    Preferences::GetBool(kGamepadEventsEnabledPref, false);
+  nsCOMPtr<nsIObserverService> observerService =
+    mozilla::services::GetObserverService();
+
+  if (NS_WARN_IF(!observerService)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsresult rv;
+  rv = observerService->AddObserver(this,
+                                    NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID,
+                                    false);
+
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+GamepadManager::Observe(nsISupports* aSubject,
+                        const char* aTopic,
+                        const char16_t* aData)
+{
+  nsCOMPtr<nsIObserverService> observerService =
+    mozilla::services::GetObserverService();
+  if (observerService) {
+    observerService->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID);
+  }
+  BeginShutdown();
+  return NS_OK;
+}
+
+void
+GamepadManager::StopMonitoring()
+{
+  if(mChild) {
+    mChild->SendGamepadListenerRemoved();
+    mChild = nullptr;
+  }
+  mGamepads.Clear();
+}
+
+void
+GamepadManager::BeginShutdown()
+{
+  mShuttingDown = true;
+  StopMonitoring();
+  // Don't let windows call back to unregister during shutdown
+  for (uint32_t i = 0; i < mListeners.Length(); i++) {
+    mListeners[i]->SetHasGamepadEventListener(false);
+  }
+  mListeners.Clear();
+  sShutdown = true;
+}
+
+void
+GamepadManager::AddListener(nsGlobalWindow* aWindow)
+{
+  MOZ_ASSERT(aWindow);
+  MOZ_ASSERT(aWindow->IsInnerWindow());
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (!mEnabled || mShuttingDown) {
+    return;
+  }
+
+  if (mListeners.IndexOf(aWindow) != NoIndex) {
+    return; // already exists
+  }
+
+  mListeners.AppendElement(aWindow);
+
+  // IPDL child has been created
+  if (mChild) {
+    return;
+  }
+  PBackgroundChild *actor = BackgroundChild::GetForCurrentThread();
+  //Try to get the PBackground Child actor
+  if (actor) {
+    ActorCreated(actor);
+  } else {
+    Unused << BackgroundChild::GetOrCreateForCurrentThread(this);
+  }
+}
+
+void
+GamepadManager::RemoveListener(nsGlobalWindow* aWindow)
+{
+  MOZ_ASSERT(aWindow);
+  MOZ_ASSERT(aWindow->IsInnerWindow());
+
+  if (mShuttingDown) {
+    // Doesn't matter at this point. It's possible we're being called
+    // as a result of our own destructor here, so just bail out.
+    return;
+  }
+
+  if (mListeners.IndexOf(aWindow) == NoIndex) {
+    return; // doesn't exist
+  }
+
+  mListeners.RemoveElement(aWindow);
+
+  if (mListeners.IsEmpty()) {
+    StopMonitoring();
+  }
+}
+
+already_AddRefed<Gamepad>
+GamepadManager::GetGamepad(uint32_t aIndex) const
+{
+  RefPtr<Gamepad> gamepad;
+  if (mGamepads.Get(aIndex, getter_AddRefs(gamepad))) {
+    return gamepad.forget();
+  }
+
+  return nullptr;
+}
+
+void
+GamepadManager::AddGamepad(uint32_t aIndex,
+                           const nsAString& aId,
+                           GamepadMappingType aMapping,
+                           uint32_t aNumButtons,
+                           uint32_t aNumAxes)
+{
+  //TODO: bug 852258: get initial button/axis state
+  RefPtr<Gamepad> gamepad =
+    new Gamepad(nullptr,
+                aId,
+                0, // index is set by global window
+                aMapping,
+                aNumButtons,
+                aNumAxes);
+
+  // We store the gamepad related to its index given by the parent process,
+  // and no duplicate index is allowed.
+  MOZ_ASSERT(!mGamepads.Get(aIndex, nullptr));
+  mGamepads.Put(aIndex, gamepad);
+  NewConnectionEvent(aIndex, true);
+}
+
+void
+GamepadManager::RemoveGamepad(uint32_t aIndex)
+{
+  RefPtr<Gamepad> gamepad = GetGamepad(aIndex);
+  if (!gamepad) {
+    NS_WARNING("Trying to delete gamepad with invalid index");
+    return;
+  }
+  gamepad->SetConnected(false);
+  NewConnectionEvent(aIndex, false);
+  mGamepads.Remove(aIndex);
+}
+
+void
+GamepadManager::NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed,
+                               double aValue)
+{
+  if (mShuttingDown) {
+    return;
+  }
+
+  RefPtr<Gamepad> gamepad = GetGamepad(aIndex);
+  if (!gamepad) {
+    return;
+  }
+
+  gamepad->SetButton(aButton, aPressed, aValue);
+
+  // Hold on to listeners in a separate array because firing events
+  // can mutate the mListeners array.
+  nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners);
+  MOZ_ASSERT(!listeners.IsEmpty());
+
+  for (uint32_t i = 0; i < listeners.Length(); i++) {
+
+    MOZ_ASSERT(listeners[i]->IsInnerWindow());
+
+    // Only send events to non-background windows
+    if (!listeners[i]->AsInner()->IsCurrentInnerWindow() ||
+        listeners[i]->GetOuterWindow()->IsBackground()) {
+      continue;
+    }
+
+    bool firstTime = MaybeWindowHasSeenGamepad(listeners[i], aIndex);
+
+    RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aIndex);
+    if (listenerGamepad) {
+      listenerGamepad->SetButton(aButton, aPressed, aValue);
+      if (firstTime) {
+        FireConnectionEvent(listeners[i], listenerGamepad, true);
+      }
+      if (mNonstandardEventsEnabled) {
+        // Fire event
+        FireButtonEvent(listeners[i], listenerGamepad, aButton, aValue);
+      }
+    }
+  }
+}
+
+void
+GamepadManager::FireButtonEvent(EventTarget* aTarget,
+                                Gamepad* aGamepad,
+                                uint32_t aButton,
+                                double aValue)
+{
+  nsString name = aValue == 1.0L ? NS_LITERAL_STRING("gamepadbuttondown") :
+                                   NS_LITERAL_STRING("gamepadbuttonup");
+  GamepadButtonEventInit init;
+  init.mBubbles = false;
+  init.mCancelable = false;
+  init.mGamepad = aGamepad;
+  init.mButton = aButton;
+  RefPtr<GamepadButtonEvent> event =
+    GamepadButtonEvent::Constructor(aTarget, name, init);
+
+  event->SetTrusted(true);
+
+  bool defaultActionEnabled = true;
+  aTarget->DispatchEvent(event, &defaultActionEnabled);
+}
+
+void
+GamepadManager::NewAxisMoveEvent(uint32_t aIndex, uint32_t aAxis, double aValue)
+{
+  if (mShuttingDown) {
+    return;
+  }
+
+  RefPtr<Gamepad> gamepad = GetGamepad(aIndex);
+  if (!gamepad) {
+    return;
+  }
+  gamepad->SetAxis(aAxis, aValue);
+
+  // Hold on to listeners in a separate array because firing events
+  // can mutate the mListeners array.
+  nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners);
+  MOZ_ASSERT(!listeners.IsEmpty());
+
+  for (uint32_t i = 0; i < listeners.Length(); i++) {
+
+    MOZ_ASSERT(listeners[i]->IsInnerWindow());
+
+    // Only send events to non-background windows
+    if (!listeners[i]->AsInner()->IsCurrentInnerWindow() ||
+        listeners[i]->GetOuterWindow()->IsBackground()) {
+      continue;
+    }
+
+    bool firstTime = MaybeWindowHasSeenGamepad(listeners[i], aIndex);
+
+    RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aIndex);
+    if (listenerGamepad) {
+      listenerGamepad->SetAxis(aAxis, aValue);
+      if (firstTime) {
+        FireConnectionEvent(listeners[i], listenerGamepad, true);
+      }
+      if (mNonstandardEventsEnabled) {
+        // Fire event
+        FireAxisMoveEvent(listeners[i], listenerGamepad, aAxis, aValue);
+      }
+    }
+  }
+}
+
+void
+GamepadManager::FireAxisMoveEvent(EventTarget* aTarget,
+                                  Gamepad* aGamepad,
+                                  uint32_t aAxis,
+                                  double aValue)
+{
+  GamepadAxisMoveEventInit init;
+  init.mBubbles = false;
+  init.mCancelable = false;
+  init.mGamepad = aGamepad;
+  init.mAxis = aAxis;
+  init.mValue = aValue;
+  RefPtr<GamepadAxisMoveEvent> event =
+    GamepadAxisMoveEvent::Constructor(aTarget,
+                                      NS_LITERAL_STRING("gamepadaxismove"),
+                                      init);
+
+  event->SetTrusted(true);
+
+  bool defaultActionEnabled = true;
+  aTarget->DispatchEvent(event, &defaultActionEnabled);
+}
+
+void
+GamepadManager::NewConnectionEvent(uint32_t aIndex, bool aConnected)
+{
+  if (mShuttingDown) {
+    return;
+  }
+
+  RefPtr<Gamepad> gamepad = GetGamepad(aIndex);
+  if (!gamepad) {
+    return;
+  }
+
+  // Hold on to listeners in a separate array because firing events
+  // can mutate the mListeners array.
+  nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners);
+  MOZ_ASSERT(!listeners.IsEmpty());
+
+  if (aConnected) {
+    for (uint32_t i = 0; i < listeners.Length(); i++) {
+
+      MOZ_ASSERT(listeners[i]->IsInnerWindow());
+
+      // Only send events to non-background windows
+      if (!listeners[i]->AsInner()->IsCurrentInnerWindow() ||
+          listeners[i]->GetOuterWindow()->IsBackground()) {
+        continue;
+      }
+
+      // We don't fire a connected event here unless the window
+      // has seen input from at least one device.
+      if (!listeners[i]->HasSeenGamepadInput()) {
+        continue;
+      }
+
+      SetWindowHasSeenGamepad(listeners[i], aIndex);
+
+      RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aIndex);
+      if (listenerGamepad) {
+        // Fire event
+        FireConnectionEvent(listeners[i], listenerGamepad, aConnected);
+      }
+    }
+  } else {
+    // For disconnection events, fire one at every window that has received
+    // data from this gamepad.
+    for (uint32_t i = 0; i < listeners.Length(); i++) {
+
+      // Even background windows get these events, so we don't have to
+      // deal with the hassle of syncing the state of removed gamepads.
+
+      if (WindowHasSeenGamepad(listeners[i], aIndex)) {
+        RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aIndex);
+        if (listenerGamepad) {
+          listenerGamepad->SetConnected(false);
+          // Fire event
+          FireConnectionEvent(listeners[i], listenerGamepad, false);
+          listeners[i]->RemoveGamepad(aIndex);
+        }
+      }
+    }
+  }
+}
+
+void
+GamepadManager::FireConnectionEvent(EventTarget* aTarget,
+                                    Gamepad* aGamepad,
+                                    bool aConnected)
+{
+  nsString name = aConnected ? NS_LITERAL_STRING("gamepadconnected") :
+                               NS_LITERAL_STRING("gamepaddisconnected");
+  GamepadEventInit init;
+  init.mBubbles = false;
+  init.mCancelable = false;
+  init.mGamepad = aGamepad;
+  RefPtr<GamepadEvent> event =
+    GamepadEvent::Constructor(aTarget, name, init);
+
+  event->SetTrusted(true);
+
+  bool defaultActionEnabled = true;
+  aTarget->DispatchEvent(event, &defaultActionEnabled);
+}
+
+void
+GamepadManager::SyncGamepadState(uint32_t aIndex, Gamepad* aGamepad)
+{
+  if (mShuttingDown || !mEnabled) {
+    return;
+  }
+
+  RefPtr<Gamepad> gamepad = GetGamepad(aIndex);
+  if (!gamepad) {
+    return;
+  }
+
+  aGamepad->SyncState(gamepad);
+}
+
+// static
+bool
+GamepadManager::IsServiceRunning()
+{
+  return !!gGamepadManagerSingleton;
+}
+
+// static
+already_AddRefed<GamepadManager>
+GamepadManager::GetService()
+{
+  if (sShutdown) {
+    return nullptr;
+  }
+
+  if (!gGamepadManagerSingleton) {
+    RefPtr<GamepadManager> manager = new GamepadManager();
+    nsresult rv = manager->Init();
+    if(NS_WARN_IF(NS_FAILED(rv))) {
+      return nullptr;
+    }
+    gGamepadManagerSingleton = manager;
+    ClearOnShutdown(&gGamepadManagerSingleton);
+  }
+
+  RefPtr<GamepadManager> service(gGamepadManagerSingleton);
+  return service.forget();
+}
+
+// static
+bool
+GamepadManager::IsAPIEnabled() {
+  return Preferences::GetBool(kGamepadEnabledPref, false);
+}
+
+bool
+GamepadManager::MaybeWindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex)
+{
+  if (!WindowHasSeenGamepad(aWindow, aIndex)) {
+    // This window hasn't seen this gamepad before, so
+    // send a connection event first.
+    SetWindowHasSeenGamepad(aWindow, aIndex);
+    return true;
+  }
+  return false;
+}
+
+bool
+GamepadManager::WindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex) const
+{
+  RefPtr<Gamepad> gamepad = aWindow->GetGamepad(aIndex);
+  return gamepad != nullptr;
+}
+
+void
+GamepadManager::SetWindowHasSeenGamepad(nsGlobalWindow* aWindow,
+                                        uint32_t aIndex,
+                                        bool aHasSeen)
+{
+  MOZ_ASSERT(aWindow);
+  MOZ_ASSERT(aWindow->IsInnerWindow());
+
+  if (mListeners.IndexOf(aWindow) == NoIndex) {
+    // This window isn't even listening for gamepad events.
+    return;
+  }
+
+  if (aHasSeen) {
+    aWindow->SetHasSeenGamepadInput(true);
+    nsCOMPtr<nsISupports> window = ToSupports(aWindow);
+    RefPtr<Gamepad> gamepad = GetGamepad(aIndex);
+    if (!gamepad) {
+      return;
+    }
+    RefPtr<Gamepad> clonedGamepad = gamepad->Clone(window);
+    aWindow->AddGamepad(aIndex, clonedGamepad);
+  } else {
+    aWindow->RemoveGamepad(aIndex);
+  }
+}
+
+void
+GamepadManager::Update(const GamepadChangeEvent& aEvent)
+{
+  if (aEvent.type() == GamepadChangeEvent::TGamepadAdded) {
+    const GamepadAdded& a = aEvent.get_GamepadAdded();
+    AddGamepad(a.index(), a.id(),
+               static_cast<GamepadMappingType>(a.mapping()),
+               a.num_buttons(), a.num_axes());
+    return;
+  }
+  if (aEvent.type() == GamepadChangeEvent::TGamepadRemoved) {
+    const GamepadRemoved& a = aEvent.get_GamepadRemoved();
+    RemoveGamepad(a.index());
+    return;
+  }
+  if (aEvent.type() == GamepadChangeEvent::TGamepadButtonInformation) {
+    const GamepadButtonInformation& a = aEvent.get_GamepadButtonInformation();
+    NewButtonEvent(a.index(), a.button(), a.pressed(), a.value());
+    return;
+  }
+  if (aEvent.type() == GamepadChangeEvent::TGamepadAxisInformation) {
+    const GamepadAxisInformation& a = aEvent.get_GamepadAxisInformation();
+    NewAxisMoveEvent(a.index(), a.axis(), a.value());
+    return;
+  }
+
+  MOZ_CRASH("We shouldn't be here!");
+
+}
+
+//Override nsIIPCBackgroundChildCreateCallback
+void
+GamepadManager::ActorCreated(PBackgroundChild *aActor)
+{
+  MOZ_ASSERT(aActor);
+  GamepadEventChannelChild *child = new GamepadEventChannelChild();
+  PGamepadEventChannelChild *initedChild =
+    aActor->SendPGamepadEventChannelConstructor(child);
+  if (NS_WARN_IF(!initedChild)) {
+    ActorFailed();
+    return;
+  }
+  MOZ_ASSERT(initedChild == child);
+  mChild = child;
+  mChild->SendGamepadListenerAdded();
+}
+
+//Override nsIIPCBackgroundChildCreateCallback
+void
+GamepadManager::ActorFailed()
+{
+  MOZ_CRASH("Gamepad IPC actor create failed!");
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/GamepadManager.h
@@ -0,0 +1,142 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_GamepadManager_h_
+#define mozilla_dom_GamepadManager_h_
+
+#include "nsIIPCBackgroundChildCreateCallback.h"
+#include "nsIObserver.h"
+// Needed for GamepadMappingType
+#include "mozilla/dom/GamepadBinding.h"
+
+class nsGlobalWindow;
+
+namespace mozilla {
+namespace dom {
+
+class EventTarget;
+class Gamepad;
+class GamepadChangeEvent;
+class GamepadEventChannelChild;
+
+class GamepadManager final : public nsIObserver,
+                             public nsIIPCBackgroundChildCreateCallback
+{
+ public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIOBSERVER
+  NS_DECL_NSIIPCBACKGROUNDCHILDCREATECALLBACK
+
+  // Returns true if we actually have a service up and running
+  static bool IsServiceRunning();
+  // Get the singleton service
+  static already_AddRefed<GamepadManager> GetService();
+  // Return true if the API is preffed on.
+  static bool IsAPIEnabled();
+
+  void BeginShutdown();
+  void StopMonitoring();
+
+  // Indicate that |aWindow| wants to receive gamepad events.
+  void AddListener(nsGlobalWindow* aWindow);
+  // Indicate that |aWindow| should no longer receive gamepad events.
+  void RemoveListener(nsGlobalWindow* aWindow);
+
+  // Add a gamepad to the list of known gamepads.
+  void AddGamepad(uint32_t aIndex, const nsAString& aID, GamepadMappingType aMapping,
+                  uint32_t aNumButtons, uint32_t aNumAxes);
+
+  // Remove the gamepad at |aIndex| from the list of known gamepads.
+  void RemoveGamepad(uint32_t aIndex);
+
+  // Update the state of |aButton| for the gamepad at |aIndex| for all
+  // windows that are listening and visible, and fire one of
+  // a gamepadbutton{up,down} event at them as well.
+  // aPressed is used for digital buttons, aValue is for analog buttons.
+  void NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed,
+                      double aValue);
+
+  // Update the state of |aAxis| for the gamepad at |aIndex| for all
+  // windows that are listening and visible, and fire a gamepadaxismove
+  // event at them as well.
+  void NewAxisMoveEvent(uint32_t aIndex, uint32_t aAxis, double aValue);
+
+  // Synchronize the state of |aGamepad| to match the gamepad stored at |aIndex|
+  void SyncGamepadState(uint32_t aIndex, Gamepad* aGamepad);
+
+  // Returns gamepad object if index exists, null otherwise
+  already_AddRefed<Gamepad> GetGamepad(uint32_t aIndex) const;
+
+  // Receive GamepadChangeEvent messages from parent process to fire DOM events
+  void Update(const GamepadChangeEvent& aGamepadEvent);
+
+ protected:
+  GamepadManager();
+  ~GamepadManager() {};
+
+  // Fire a gamepadconnected or gamepaddisconnected event for the gamepad
+  // at |aIndex| to all windows that are listening and have received
+  // gamepad input.
+  void NewConnectionEvent(uint32_t aIndex, bool aConnected);
+
+  // Fire a gamepadaxismove event to the window at |aTarget| for |aGamepad|.
+  void FireAxisMoveEvent(EventTarget* aTarget,
+                         Gamepad* aGamepad,
+                         uint32_t axis,
+                         double value);
+
+  // Fire one of gamepadbutton{up,down} event at the window at |aTarget| for
+  // |aGamepad|.
+  void FireButtonEvent(EventTarget* aTarget,
+                       Gamepad* aGamepad,
+                       uint32_t aButton,
+                       double aValue);
+
+  // Fire one of gamepad{connected,disconnected} event at the window at
+  // |aTarget| for |aGamepad|.
+  void FireConnectionEvent(EventTarget* aTarget,
+                           Gamepad* aGamepad,
+                           bool aConnected);
+
+  // true if this feature is enabled in preferences
+  bool mEnabled;
+  // true if non-standard events are enabled in preferences
+  bool mNonstandardEventsEnabled;
+  // true when shutdown has begun
+  bool mShuttingDown;
+
+  // Gamepad IPDL child
+  // This pointer is only used by this singleton instance and
+  // will be destroyed during the IPDL shutdown chain, so we
+  // don't need to refcount it here
+  GamepadEventChannelChild MOZ_NON_OWNING_REF *mChild;
+
+ private:
+
+  nsresult Init();
+
+  bool MaybeWindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex);
+  // Returns true if we have already sent data from this gamepad
+  // to this window. This should only return true if the user
+  // explicitly interacted with a gamepad while this window
+  // was focused, by pressing buttons or similar actions.
+  bool WindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex) const;
+  // Indicate that a window has recieved data from a gamepad.
+  void SetWindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex,
+                               bool aHasSeen = true);
+
+  // Gamepads connected to the system. Copies of these are handed out
+  // to each window.
+  nsRefPtrHashtable<nsUint32HashKey, Gamepad> mGamepads;
+  // Inner windows that are listening for gamepad events.
+  // has been sent to that window.
+  nsTArray<RefPtr<nsGlobalWindow>> mListeners;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_GamepadManager_h_
--- a/dom/gamepad/GamepadMonitoring.cpp
+++ b/dom/gamepad/GamepadMonitoring.cpp
@@ -1,33 +1,32 @@
 /* -*- 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 "mozilla/dom/GamepadMonitoring.h"
-#include "mozilla/dom/GamepadFunctions.h"
-#include "mozilla/dom/PContentParent.h"
+#include "mozilla/dom/GamepadPlatformService.h"
+#include "mozilla/ipc/BackgroundParent.h"
+
+using namespace mozilla::ipc;
 
 namespace mozilla {
 namespace dom {
 
-using namespace GamepadFunctions;
-
 void
 MaybeStopGamepadMonitoring()
 {
-  MOZ_ASSERT(NS_IsMainThread());
-  MOZ_ASSERT(XRE_IsParentProcess());
-  nsTArray<ContentParent*> t;
-  ContentParent::GetAll(t);
-  for(uint32_t i = 0; i < t.Length(); ++i) {
-    if (t[i]->HasGamepadListener()) {
-      return;
-    }
+  AssertIsOnBackgroundThread();
+  RefPtr<GamepadPlatformService> service =
+    GamepadPlatformService::GetParentService();
+  MOZ_ASSERT(service);
+  if(service->HasGamepadListeners()) {
+    return;
   }
   StopGamepadMonitoring();
-  ResetGamepadIndexes();
+  service->ResetGamepadIndexes();
+  service->MaybeShutdown();
 }
 
 } // namespace dom
 } // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/GamepadPlatformService.cpp
@@ -0,0 +1,234 @@
+/* -*- 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 "mozilla/dom/GamepadPlatformService.h"
+
+#include "mozilla/dom/GamepadEventChannelParent.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/unused.h"
+
+#include "nsCOMPtr.h"
+#include "nsHashKeys.h"
+#include "nsIThread.h"
+
+using namespace mozilla::ipc;
+
+namespace mozilla {
+namespace dom {
+
+namespace {
+
+// This is the singleton instance of GamepadPlatformService, can be called
+// by both background and monitor thread.
+StaticRefPtr<GamepadPlatformService> gGamepadPlatformServiceSingleton;
+
+} //namepsace
+
+GamepadPlatformService::GamepadPlatformService()
+  : mGamepadIndex(0),
+    mMutex("mozilla::dom::GamepadPlatformService")
+{}
+
+GamepadPlatformService::~GamepadPlatformService()
+{
+  Cleanup();
+}
+
+// static
+already_AddRefed<GamepadPlatformService>
+GamepadPlatformService::GetParentService()
+{
+  //GamepadPlatformService can only be accessed in parent process
+  MOZ_ASSERT(XRE_IsParentProcess());
+  if (!gGamepadPlatformServiceSingleton) {
+    // Only Background Thread can create new GamepadPlatformService instance.
+    if (IsOnBackgroundThread()) {
+      gGamepadPlatformServiceSingleton = new GamepadPlatformService();
+    } else {
+      return nullptr;
+    }
+  }
+  RefPtr<GamepadPlatformService> service(gGamepadPlatformServiceSingleton);
+  return service.forget();
+}
+
+template<class T>
+void
+GamepadPlatformService::NotifyGamepadChange(const T& aInfo)
+{
+  // This method is called by monitor populated in
+  // platform-dependent backends
+  MOZ_ASSERT(XRE_IsParentProcess());
+  MOZ_ASSERT(!NS_IsMainThread());
+
+  GamepadChangeEvent e(aInfo);
+
+  // mChannelParents may be accessed by background thread in the
+  // same time, we use mutex to prevent possible race condtion
+  MutexAutoLock autoLock(mMutex);
+  for(uint32_t i = 0; i < mChannelParents.Length(); ++i) {
+    mChannelParents[i]->DispatchUpdateEvent(e);
+  }
+}
+
+uint32_t
+GamepadPlatformService::AddGamepad(const char* aID,
+                                   GamepadMappingType aMapping,
+                                   uint32_t aNumButtons, uint32_t aNumAxes)
+{
+  // This method is called by monitor thread populated in
+  // platform-dependent backends
+  MOZ_ASSERT(XRE_IsParentProcess());
+  MOZ_ASSERT(!NS_IsMainThread());
+
+  uint32_t index = ++mGamepadIndex;
+  GamepadAdded a(NS_ConvertUTF8toUTF16(nsDependentCString(aID)), index,
+                 (uint32_t)aMapping, aNumButtons, aNumAxes);
+  NotifyGamepadChange<GamepadAdded>(a);
+  return index;
+}
+
+void
+GamepadPlatformService::RemoveGamepad(uint32_t aIndex)
+{
+  // This method is called by monitor thread populated in
+  // platform-dependent backends
+  MOZ_ASSERT(XRE_IsParentProcess());
+  MOZ_ASSERT(!NS_IsMainThread());
+  GamepadRemoved a(aIndex);
+  NotifyGamepadChange<GamepadRemoved>(a);
+}
+
+void
+GamepadPlatformService::NewButtonEvent(uint32_t aIndex, uint32_t aButton,
+                                       bool aPressed, double aValue)
+{
+  // This method is called by monitor thread populated in
+  // platform-dependent backends
+  MOZ_ASSERT(XRE_IsParentProcess());
+  MOZ_ASSERT(!NS_IsMainThread());
+  GamepadButtonInformation a(aIndex, aButton, aPressed, aValue);
+  NotifyGamepadChange<GamepadButtonInformation>(a);
+}
+
+void
+GamepadPlatformService::NewButtonEvent(uint32_t aIndex, uint32_t aButton,
+                                       bool aPressed)
+{
+  // This method is called by monitor thread populated in
+  // platform-dependent backends
+  MOZ_ASSERT(XRE_IsParentProcess());
+  MOZ_ASSERT(!NS_IsMainThread());
+  // When only a digital button is available the value will be synthesized.
+  NewButtonEvent(aIndex, aButton, aPressed, aPressed ? 1.0L : 0.0L);
+}
+
+void
+GamepadPlatformService::NewAxisMoveEvent(uint32_t aIndex, uint32_t aAxis,
+                                         double aValue)
+{
+  // This method is called by monitor thread populated in
+  // platform-dependent backends
+  MOZ_ASSERT(XRE_IsParentProcess());
+  MOZ_ASSERT(!NS_IsMainThread());
+  GamepadAxisInformation a(aIndex, aAxis, aValue);
+  NotifyGamepadChange<GamepadAxisInformation>(a);
+}
+
+void
+GamepadPlatformService::ResetGamepadIndexes()
+{
+  // This method is called by monitor thread populated in
+  // platform-dependent backends
+  MOZ_ASSERT(XRE_IsParentProcess());
+  MOZ_ASSERT(!NS_IsMainThread());
+  mGamepadIndex = 0;
+}
+
+void
+GamepadPlatformService::AddChannelParent(GamepadEventChannelParent* aParent)
+{
+  // mChannelParents can only be modified once GamepadEventChannelParent
+  // is created or removed in Background thread
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aParent);
+  MOZ_ASSERT(!mChannelParents.Contains(aParent));
+
+  // We use mutex here to prevent race condition with monitor thread
+  MutexAutoLock autoLock(mMutex);
+  mChannelParents.AppendElement(aParent);
+}
+
+void
+GamepadPlatformService::RemoveChannelParent(GamepadEventChannelParent* aParent)
+{
+  // mChannelParents can only be modified once GamepadEventChannelParent
+  // is created or removed in Background thread
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aParent);
+  MOZ_ASSERT(mChannelParents.Contains(aParent));
+
+  // We use mutex here to prevent race condition with monitor thread
+  MutexAutoLock autoLock(mMutex);
+  mChannelParents.RemoveElement(aParent);
+}
+
+bool
+GamepadPlatformService::HasGamepadListeners()
+{
+  // mChannelParents may be accessed by background thread in the
+  // same time, we use mutex to prevent possible race condtion
+  AssertIsOnBackgroundThread();
+
+  // We use mutex here to prevent race condition with monitor thread
+  MutexAutoLock autoLock(mMutex);
+  for (uint32_t i = 0; i < mChannelParents.Length(); i++) {
+    if(mChannelParents[i]->HasGamepadListener()) {
+      return true;
+    }
+  }
+  return false;
+}
+
+void
+GamepadPlatformService::MaybeShutdown()
+{
+  // This method is invoked in MaybeStopGamepadMonitoring when
+  // an IPDL channel is going to be destroyed
+  AssertIsOnBackgroundThread();
+
+  // We have to release gGamepadPlatformServiceSingleton ouside
+  // the mutex as well as making upcoming GetParentService() call
+  // recreate new singleton, so we use this RefPtr to temporarily
+  // hold the reference, postponing the release process until this
+  // method ends.
+  RefPtr<GamepadPlatformService> kungFuDeathGrip;
+
+  bool isChannelParentEmpty;
+  {
+    MutexAutoLock autoLock(mMutex);
+    isChannelParentEmpty = mChannelParents.IsEmpty();
+    if(isChannelParentEmpty) {
+      kungFuDeathGrip = gGamepadPlatformServiceSingleton;
+      gGamepadPlatformServiceSingleton = nullptr;
+    }
+  }
+}
+
+void
+GamepadPlatformService::Cleanup()
+{
+  // This method is called when GamepadPlatformService is
+  // successfully distructed in background thread
+  AssertIsOnBackgroundThread();
+
+  MutexAutoLock autoLock(mMutex);
+  mChannelParents.Clear();
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/GamepadPlatformService.h
@@ -0,0 +1,95 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_GamepadPlatformService_h_
+#define mozilla_dom_GamepadPlatformService_h_
+
+#include "mozilla/dom/GamepadBinding.h"
+
+#include "mozilla/Mutex.h"
+#include "mozilla/StaticPtr.h"
+
+namespace mozilla {
+namespace dom {
+
+class GamepadEventChannelParent;
+
+// Platform Service for building and transmitting IPDL messages
+// through the HAL sandbox. Used by platform specific
+// Gamepad implementations
+//
+// This class can be accessed by the following 2 threads :
+// 1. Background thread:
+//    This thread takes charge of IPDL communications
+//    between here and DOM side
+//
+// 2. Monitor Thread:
+//    This thread is populated in platform-dependent backends, which
+//    is in charge of processing gamepad hardware events from OS
+class GamepadPlatformService final
+{
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GamepadPlatformService)
+ public:
+  //Get the singleton service
+  static already_AddRefed<GamepadPlatformService> GetParentService();
+
+  // Add a gamepad to the list of known gamepads, and return its index.
+  uint32_t AddGamepad(const char* aID, GamepadMappingType aMapping,
+                      uint32_t aNumButtons, uint32_t aNumAxes);
+  // Remove the gamepad at |aIndex| from the list of known gamepads.
+  void RemoveGamepad(uint32_t aIndex);
+
+  // Update the state of |aButton| for the gamepad at |aIndex| for all
+  // windows that are listening and visible, and fire one of
+  // a gamepadbutton{up,down} event at them as well.
+  // aPressed is used for digital buttons, aValue is for analog buttons.
+  void NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed,
+                      double aValue);
+  // When only a digital button is available the value will be synthesized.
+  void NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed);
+
+  // Update the state of |aAxis| for the gamepad at |aIndex| for all
+  // windows that are listening and visible, and fire a gamepadaxismove
+  // event at them as well.
+  void NewAxisMoveEvent(uint32_t aIndex, uint32_t aAxis, double aValue);
+
+  // When shutting down the platform communications for gamepad, also reset the
+  // indexes.
+  void ResetGamepadIndexes();
+
+  //Add IPDL parent instance
+  void AddChannelParent(GamepadEventChannelParent* aParent);
+
+  //Remove IPDL parent instance
+  void RemoveChannelParent(GamepadEventChannelParent* aParent);
+
+  bool HasGamepadListeners();
+
+  void MaybeShutdown();
+
+ private:
+  GamepadPlatformService();
+  ~GamepadPlatformService();
+  template<class T> void NotifyGamepadChange(const T& aInfo);
+  void Cleanup();
+
+  // mGamepadIndex can only be accessed by monitor thread
+  uint32_t mGamepadIndex;
+
+  // mChannelParents stores all the GamepadEventChannelParent instances
+  // which may be accessed by both background thread and monitor thread
+  // simultaneously, so we have a mutex to prevent race condition
+  nsTArray<RefPtr<GamepadEventChannelParent>> mChannelParents;
+
+  // This mutex protects mChannelParents from race condition
+  // between background and monitor thread
+  Mutex mMutex;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
--- a/dom/gamepad/GamepadServiceTest.cpp
+++ b/dom/gamepad/GamepadServiceTest.cpp
@@ -1,86 +1,278 @@
 /* -*- 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 "GamepadServiceTest.h"
-#include "mozilla/dom/GamepadService.h"
-#include "mozilla/dom/GamepadFunctions.h"
+
+#include "mozilla/ErrorResult.h"
+#include "mozilla/unused.h"
+
+#include "mozilla/dom/GamepadManager.h"
+#include "mozilla/dom/GamepadPlatformService.h"
+#include "mozilla/dom/GamepadServiceTestBinding.h"
+#include "mozilla/dom/GamepadTestChannelChild.h"
 
-using namespace mozilla::dom;
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+
+#include "mozilla/unused.h"
+
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+
+namespace mozilla {
+namespace dom {
 
 /*
  * Implementation of the test service. This is just to provide a simple binding
- * of the GamepadService to JavaScript via XPCOM so that we can write Mochitests
+ * of the GamepadService to JavaScript via WebIDL so that we can write Mochitests
  * that add and remove fake gamepads, avoiding the platform-specific backends.
  */
-NS_IMPL_ISUPPORTS(GamepadServiceTest, nsIGamepadServiceTest)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(GamepadServiceTest)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(GamepadServiceTest,
+                                                  DOMEventTargetHelper)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
-GamepadServiceTest* GamepadServiceTest::sSingleton = nullptr;
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(GamepadServiceTest,
+                                                DOMEventTargetHelper)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(GamepadServiceTest)
+  NS_INTERFACE_MAP_ENTRY(nsIIPCBackgroundChildCreateCallback)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_ADDREF_INHERITED(GamepadServiceTest, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(GamepadServiceTest, DOMEventTargetHelper)
 
 // static
 already_AddRefed<GamepadServiceTest>
-GamepadServiceTest::CreateService()
+GamepadServiceTest::CreateTestService(nsPIDOMWindowInner* aWindow)
 {
-  if (sSingleton == nullptr) {
-    sSingleton = new GamepadServiceTest();
-  }
-  RefPtr<GamepadServiceTest> service = sSingleton;
+  MOZ_ASSERT(aWindow);
+  RefPtr<GamepadServiceTest> service = new GamepadServiceTest(aWindow);
+  service->InitPBackgroundActor();
   return service.forget();
 }
 
-GamepadServiceTest::GamepadServiceTest() :
-  mService(GamepadService::GetService())
+void
+GamepadServiceTest::Shutdown()
 {
+  MOZ_ASSERT(!mShuttingDown);
+  mShuttingDown = true;
+  DestroyPBackgroundActor();
+  mWindow = nullptr;
 }
 
-GamepadServiceTest::~GamepadServiceTest()
+GamepadServiceTest::GamepadServiceTest(nsPIDOMWindowInner* aWindow)
+  : mService(GamepadManager::GetService()),
+    mWindow(aWindow),
+    mEventNumber(0),
+    mShuttingDown(false),
+    mChild(nullptr)
+{}
+
+GamepadServiceTest::~GamepadServiceTest() {}
+
+void
+GamepadServiceTest::InitPBackgroundActor()
 {
+  MOZ_ASSERT(!mChild);
+  PBackgroundChild *actor = BackgroundChild::GetForCurrentThread();
+  //Try to get the PBackground Child actor
+  if (actor) {
+    ActorCreated(actor);
+  } else {
+    Unused << BackgroundChild::GetOrCreateForCurrentThread(this);
+  }
 }
 
-NS_IMETHODIMP
-GamepadServiceTest::AddGamepad(const char* aID,
+void
+GamepadServiceTest::DestroyPBackgroundActor()
+{
+  if (mChild) {
+    // If mChild exists, which means that IPDL channel
+    // has been created, our pending operations should
+    // be empty.
+    MOZ_ASSERT(mPendingOperations.IsEmpty());
+    mChild->SendShutdownChannel();
+    mChild = nullptr;
+  } else {
+    // If the IPDL channel has not been created and we
+    // want to destroy it now, just cancel all pending
+    // operations.
+    mPendingOperations.Clear();
+  }
+}
+
+already_AddRefed<Promise>
+GamepadServiceTest::AddGamepad(const nsAString& aID,
                                uint32_t aMapping,
                                uint32_t aNumButtons,
                                uint32_t aNumAxes,
-                               uint32_t* aGamepadIndex)
+                               ErrorResult& aRv)
 {
-  *aGamepadIndex = GamepadFunctions::AddGamepad(aID,
-                                                static_cast<GamepadMappingType>(aMapping),
-                                                aNumButtons,
-                                                aNumAxes);
-  return NS_OK;
+  if (mShuttingDown) {
+    return nullptr;
+  }
+
+  GamepadAdded a(nsString(aID), 0,
+                (uint32_t)aMapping, aNumButtons, aNumAxes);
+  GamepadChangeEvent e(a);
+  nsCOMPtr<nsIGlobalObject> go = do_QueryInterface(mWindow);
+
+  RefPtr<Promise> p = Promise::Create(go, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  uint32_t id = ++mEventNumber;
+  if (mChild) {
+    mChild->AddPromise(id, p);
+    mChild->SendGamepadTestEvent(id, e);
+  } else {
+    PendingOperation op(id, e, p);
+    mPendingOperations.AppendElement(op);
+  }
+  return p.forget();
 }
 
-NS_IMETHODIMP GamepadServiceTest::RemoveGamepad(uint32_t aIndex)
+void
+GamepadServiceTest::RemoveGamepad(uint32_t aIndex)
 {
-  GamepadFunctions::RemoveGamepad(aIndex);
-  return NS_OK;
+  if (mShuttingDown) {
+    return;
+  }
+
+  GamepadRemoved a(aIndex);
+  GamepadChangeEvent e(a);
+
+  uint32_t id = ++mEventNumber;
+  if (mChild) {
+    mChild->SendGamepadTestEvent(id, e);
+  } else {
+    PendingOperation op(id, e);
+    mPendingOperations.AppendElement(op);
+  }
+}
+
+void
+GamepadServiceTest::NewButtonEvent(uint32_t aIndex,
+                                   uint32_t aButton,
+                                   bool aPressed)
+{
+  if (mShuttingDown) {
+    return;
+  }
+
+  GamepadButtonInformation a(aIndex, aButton, aPressed, aPressed ? 1.0 : 0);
+  GamepadChangeEvent e(a);
+
+  uint32_t id = ++mEventNumber;
+  if (mChild) {
+    mChild->SendGamepadTestEvent(id, e);
+  } else {
+    PendingOperation op(id, e);
+    mPendingOperations.AppendElement(op);
+  }
 }
 
-NS_IMETHODIMP GamepadServiceTest::NewButtonEvent(uint32_t aIndex,
-                                                 uint32_t aButton,
-                                                 bool aPressed)
+void
+GamepadServiceTest::NewButtonValueEvent(uint32_t aIndex,
+                                        uint32_t aButton,
+                                        bool aPressed,
+                                        double aValue)
 {
-  GamepadFunctions::NewButtonEvent(aIndex, aButton, aPressed);
-  return NS_OK;
+  if (mShuttingDown) {
+    return;
+  }
+
+  GamepadButtonInformation a(aIndex, aButton, aPressed, aValue);
+  GamepadChangeEvent e(a);
+
+  uint32_t id = ++mEventNumber;
+  if (mChild) {
+    mChild->SendGamepadTestEvent(id, e);
+  } else {
+    PendingOperation op(id, e);
+    mPendingOperations.AppendElement(op);
+  }
+}
+
+void
+GamepadServiceTest::NewAxisMoveEvent(uint32_t aIndex,
+                                     uint32_t aAxis,
+                                     double aValue)
+{
+  if (mShuttingDown) {
+    return;
+  }
+
+  GamepadAxisInformation a(aIndex, aAxis, aValue);
+  GamepadChangeEvent e(a);
+
+  uint32_t id = ++mEventNumber;
+  if (mChild) {
+    mChild->SendGamepadTestEvent(id, e);
+  } else {
+    PendingOperation op(id, e);
+    mPendingOperations.AppendElement(op);
+  }
 }
 
-NS_IMETHODIMP GamepadServiceTest::NewButtonValueEvent(uint32_t aIndex,
-                                                      uint32_t aButton,
-                                                      bool aPressed,
-                                                      double aValue)
+void
+GamepadServiceTest::FlushPendingOperations()
 {
-  GamepadFunctions::NewButtonEvent(aIndex, aButton, aPressed, aValue);
-  return NS_OK;
+  for (uint32_t i=0; i < mPendingOperations.Length(); ++i) {
+    PendingOperation op = mPendingOperations[i];
+    if (op.mPromise) {
+      mChild->AddPromise(op.mID, op.mPromise);
+    }
+    mChild->SendGamepadTestEvent(op.mID, op.mEvent);
+  }
+  mPendingOperations.Clear();
 }
 
-NS_IMETHODIMP GamepadServiceTest::NewAxisMoveEvent(uint32_t aIndex,
-                                                   uint32_t aAxis,
-                                                   double aValue)
+void
+GamepadServiceTest::ActorCreated(PBackgroundChild* aActor)
 {
-  GamepadFunctions::NewAxisMoveEvent(aIndex, aAxis, aValue);
-  return NS_OK;
+  MOZ_ASSERT(aActor);
+  // If we are shutting down, we don't need to create the
+  // IPDL child/parent pair anymore.
+  if (mShuttingDown) {
+    // mPendingOperations should be cleared in
+    // DestroyPBackgroundActor()
+    MOZ_ASSERT(mPendingOperations.IsEmpty());
+    return;
+  }
+
+  mChild = new GamepadTestChannelChild();
+  PGamepadTestChannelChild* initedChild =
+    aActor->SendPGamepadTestChannelConstructor(mChild);
+  if (NS_WARN_IF(!initedChild)) {
+    ActorFailed();
+    return;
+  }
+  FlushPendingOperations();
 }
 
+void
+GamepadServiceTest::ActorFailed()
+{
+  MOZ_CRASH("Failed to create background child actor!");
+}
+
+JSObject*
+GamepadServiceTest::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto)
+{
+  return GamepadServiceTestBinding::Wrap(aCx, this, aGivenProto);
+}
+
+} // dom
+} // mozilla
--- a/dom/gamepad/GamepadServiceTest.h
+++ b/dom/gamepad/GamepadServiceTest.h
@@ -2,43 +2,86 @@
 /* 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/. */
 
 #ifndef mozilla_dom_GamepadServiceTest_h_
 #define mozilla_dom_GamepadServiceTest_h_
 
-#include "nsIGamepadServiceTest.h"
+#include "nsIIPCBackgroundChildCreateCallback.h"
 
 namespace mozilla {
 namespace dom {
 
-class GamepadService;
+class GamepadChangeEvent;
+class GamepadManager;
+class GamepadTestChannelChild;
+class Promise;
 
 // Service for testing purposes
-class GamepadServiceTest : public nsIGamepadServiceTest
+class GamepadServiceTest final : public DOMEventTargetHelper,
+                                 public nsIIPCBackgroundChildCreateCallback
 {
 public:
-  NS_DECL_ISUPPORTS
-  NS_DECL_NSIGAMEPADSERVICETEST
+  NS_DECL_NSIIPCBACKGROUNDCHILDCREATECALLBACK
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(GamepadServiceTest,
+                                           DOMEventTargetHelper)
+
+  uint32_t NoMapping() const { return 0; }
+  uint32_t StandardMapping() const { return 1; }
 
-  GamepadServiceTest();
+  already_AddRefed<Promise> AddGamepad(const nsAString& aID,
+                                       uint32_t aMapping,
+                                       uint32_t aNumButtons,
+                                       uint32_t aNumAxes,
+                                       ErrorResult& aRv);
+  void RemoveGamepad(uint32_t aIndex);
+  void NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed);
+  void NewButtonValueEvent(uint32_t aIndex, uint32_t aButton, bool aPressed, double aValue);
+  void NewAxisMoveEvent(uint32_t aIndex, uint32_t aAxis, double aValue);
+  void Shutdown();
 
-  static already_AddRefed<GamepadServiceTest> CreateService();
+  static already_AddRefed<GamepadServiceTest> CreateTestService(nsPIDOMWindowInner* aWindow);
+  nsPIDOMWindowInner* GetParentObject() const { return mWindow; }
+  JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override;
 
 private:
-  static GamepadServiceTest* sSingleton;
+
+  // We need to asynchronously create IPDL channel, it is possible that
+  // we send commands before the channel is created, so we have to buffer
+  // them until the channel is created in that case.
+  struct PendingOperation {
+    explicit PendingOperation(const uint32_t& aID,
+                              const GamepadChangeEvent& aEvent,
+                              Promise* aPromise = nullptr)
+               : mID(aID), mEvent(aEvent), mPromise(aPromise) {}
+    uint32_t mID;
+    const GamepadChangeEvent& mEvent;
+    RefPtr<Promise> mPromise;
+  };
+
   // Hold a reference to the gamepad service so we don't have to worry about
   // execution order in tests.
-  RefPtr<GamepadService> mService;
-  virtual ~GamepadServiceTest();
+  RefPtr<GamepadManager> mService;
+  nsCOMPtr<nsPIDOMWindowInner> mWindow;
+  nsTArray<PendingOperation> mPendingOperations;
+  uint32_t mEventNumber;
+  bool mShuttingDown;
+
+  // IPDL Channel for us to send test events to GamepadPlatformService, it
+  // will only be used in this singleton class and deleted during the IPDL
+  // shutdown chain
+  GamepadTestChannelChild* MOZ_NON_OWNING_REF mChild;
+
+  explicit GamepadServiceTest(nsPIDOMWindowInner* aWindow);
+  ~GamepadServiceTest();
+  void InitPBackgroundActor();
+  void DestroyPBackgroundActor();
+  void FlushPendingOperations();
+
 };
 
 } // namespace dom
 } // namespace mozilla
 
-#define NS_GAMEPAD_TEST_CID \
-{ 0xfb1fcb57, 0xebab, 0x4cf4, \
-{ 0x96, 0x3b, 0x1e, 0x4d, 0xb8, 0x52, 0x16, 0x96 } }
-#define NS_GAMEPAD_TEST_CONTRACTID "@mozilla.org/gamepad-test;1"
-
 #endif
--- a/dom/gamepad/cocoa/CocoaGamepad.cpp
+++ b/dom/gamepad/cocoa/CocoaGamepad.cpp
@@ -2,30 +2,31 @@
 /* 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/. */
 
 // mostly derived from the Allegro source code at:
 // http://alleg.svn.sourceforge.net/viewvc/alleg/allegro/branches/4.9/src/macosx/hidjoy.m?revision=13760&view=markup
 
-#include "mozilla/dom/GamepadFunctions.h"
+#include "mozilla/dom/GamepadPlatformService.h"
 #include "mozilla/ArrayUtils.h"
+#include "nsThreadUtils.h"
 #include <CoreFoundation/CoreFoundation.h>
 #include <IOKit/hid/IOHIDBase.h>
 #include <IOKit/hid/IOHIDKeys.h>
 #include <IOKit/hid/IOHIDManager.h>
 
 #include <stdio.h>
 #include <vector>
 
 namespace {
 
 using namespace mozilla;
-using namespace mozilla::dom::GamepadFunctions;
+using namespace mozilla::dom;
 using std::vector;
 
 struct Button {
   int id;
   bool analog;
   IOHIDElementRef element;
   CFIndex min;
   CFIndex max;
@@ -195,37 +196,68 @@ void Gamepad::init(IOHIDDeviceRef device
   }
 }
 
 class DarwinGamepadService {
  private:
   IOHIDManagerRef mManager;
   vector<Gamepad> mGamepads;
 
+  //Workaround to support running in background thread
+  CFRunLoopRef mMonitorRunLoop;
+  nsCOMPtr<nsIThread> mMonitorThread;
+
   static void DeviceAddedCallback(void* data, IOReturn result,
                                   void* sender, IOHIDDeviceRef device);
   static void DeviceRemovedCallback(void* data, IOReturn result,
                                     void* sender, IOHIDDeviceRef device);
   static void InputValueChangedCallback(void* data, IOReturn result,
                                         void* sender, IOHIDValueRef newValue);
 
   void DeviceAdded(IOHIDDeviceRef device);
   void DeviceRemoved(IOHIDDeviceRef device);
   void InputValueChanged(IOHIDValueRef value);
+  void StartupInternal();
 
  public:
   DarwinGamepadService();
   ~DarwinGamepadService();
   void Startup();
   void Shutdown();
+  friend class DarwinGamepadServiceStartupRunnable;
+};
+
+class DarwinGamepadServiceStartupRunnable final : public Runnable
+{
+ private:
+  ~DarwinGamepadServiceStartupRunnable() {}
+  // This Runnable schedules startup of DarwinGamepadService
+  // in a new thread, pointer to DarwinGamepadService is only
+  // used by this Runnable within its thread.
+  DarwinGamepadService MOZ_NON_OWNING_REF *mService;
+ public:
+  explicit DarwinGamepadServiceStartupRunnable(DarwinGamepadService *service)
+             : mService(service) {}
+  NS_IMETHOD Run() override
+  {
+    MOZ_ASSERT(mService);
+    mService->StartupInternal();
+    return NS_OK;
+  }
 };
 
 void
 DarwinGamepadService::DeviceAdded(IOHIDDeviceRef device)
 {
+  RefPtr<GamepadPlatformService> service =
+    GamepadPlatformService::GetParentService();
+  if (!service) {
+    return;
+  }
+
   size_t slot = size_t(-1);
   for (size_t i = 0; i < mGamepads.size(); i++) {
     if (mGamepads[i] == device)
       return;
     if (slot == size_t(-1) && mGamepads[i].empty())
       slot = i;
   }
 
@@ -245,28 +277,34 @@ DarwinGamepadService::DeviceAdded(IOHIDD
   int vendorId, productId;
   CFNumberGetValue(vendorIdRef, kCFNumberIntType, &vendorId);
   CFNumberGetValue(productIdRef, kCFNumberIntType, &productId);
   char product_name[128];
   CFStringGetCString(productRef, product_name,
                      sizeof(product_name), kCFStringEncodingASCII);
   char buffer[256];
   sprintf(buffer, "%x-%x-%s", vendorId, productId, product_name);
-  mGamepads[slot].mSuperIndex = AddGamepad(buffer,
-                                           mozilla::dom::GamepadMappingType::_empty,
-                                           (int)mGamepads[slot].numButtons(),
-                                           (int)mGamepads[slot].numAxes());
+  uint32_t index = service->AddGamepad(buffer,
+                                       mozilla::dom::GamepadMappingType::_empty,
+                                       (int)mGamepads[slot].numButtons(),
+                                       (int)mGamepads[slot].numAxes());
+  mGamepads[slot].mSuperIndex = index;
 }
 
 void
 DarwinGamepadService::DeviceRemoved(IOHIDDeviceRef device)
 {
+  RefPtr<GamepadPlatformService> service =
+    GamepadPlatformService::GetParentService();
+  if (!service) {
+    return;
+  }
   for (size_t i = 0; i < mGamepads.size(); i++) {
     if (mGamepads[i] == device) {
-      RemoveGamepad(mGamepads[i].mSuperIndex);
+      service->RemoveGamepad(mGamepads[i].mSuperIndex);
       mGamepads[i].clear();
       return;
     }
   }
 }
 
 /*
  * Given a value from a d-pad (POV hat in USB HID terminology),
@@ -305,57 +343,66 @@ UnpackDpad(int dpad_value, int min, int 
   if (value > 0 && value < 4) {
     buttons[kRight] = true;
   }
 }
 
 void
 DarwinGamepadService::InputValueChanged(IOHIDValueRef value)
 {
+  RefPtr<GamepadPlatformService> service =
+    GamepadPlatformService::GetParentService();
+  if (!service) {
+    return;
+  }
+
   uint32_t value_length = IOHIDValueGetLength(value);
   if (value_length > 4) {
     // Workaround for bizarre issue with PS3 controllers that try to return
     // massive (30+ byte) values and crash IOHIDValueGetIntegerValue
     return;
   }
   IOHIDElementRef element = IOHIDValueGetElement(value);
   IOHIDDeviceRef device = IOHIDElementGetDevice(element);
+
   for (unsigned i = 0; i < mGamepads.size(); i++) {
     Gamepad &gamepad = mGamepads[i];
     if (gamepad == device) {
       if (gamepad.isDpad(element)) {
         const dpad_buttons& oldState = gamepad.getDpadState();
         dpad_buttons newState = { false, false, false, false };
         UnpackDpad(IOHIDValueGetIntegerValue(value),
                    IOHIDElementGetLogicalMin(element),
                    IOHIDElementGetLogicalMax(element),
                    newState);
         const int numButtons = gamepad.numButtons();
         for (unsigned b = 0; b < ArrayLength(newState); b++) {
           if (newState[b] != oldState[b]) {
-            NewButtonEvent(gamepad.mSuperIndex, numButtons - 4 + b, newState[b]);
+            service->NewButtonEvent(gamepad.mSuperIndex,
+                                    numButtons - 4 + b,
+                                    newState[b]);
           }
         }
         gamepad.setDpadState(newState);
       } else if (const Axis* axis = gamepad.lookupAxis(element)) {
         double d = IOHIDValueGetIntegerValue(value);
         double v = 2.0f * (d - axis->min) /
           (double)(axis->max - axis->min) - 1.0f;
-        NewAxisMoveEvent(gamepad.mSuperIndex, axis->id, v);
+        service->NewAxisMoveEvent(gamepad.mSuperIndex, axis->id, v);
       } else if (const Button* button = gamepad.lookupButton(element)) {
         int iv = IOHIDValueGetIntegerValue(value);
         bool pressed = iv != 0;
         double v = 0;
         if (button->analog) {
           double dv = iv;
           v = (dv - button->min) / (double)(button->max - button->min);
         } else {
           v = pressed ? 1.0 : 0.0;
         }
-        NewButtonEvent(gamepad.mSuperIndex, button->id, pressed, v);
+        service->NewButtonEvent(gamepad.mSuperIndex, button->id, pressed, v);
       }
       return;
     }
   }
 }
 
 void
 DarwinGamepadService::DeviceAddedCallback(void* data, IOReturn result,
@@ -417,17 +464,18 @@ MatchingDictionary(UInt32 inUsagePage, U
 DarwinGamepadService::DarwinGamepadService() : mManager(nullptr) {}
 
 DarwinGamepadService::~DarwinGamepadService()
 {
   if (mManager != nullptr)
     CFRelease(mManager);
 }
 
-void DarwinGamepadService::Startup()
+void
+DarwinGamepadService::StartupInternal()
 {
   if (mManager != nullptr)
     return;
 
   IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault,
                                                kIOHIDOptionsTypeNone);
 
   CFMutableDictionaryRef criteria_arr[2];
@@ -474,26 +522,44 @@ void DarwinGamepadService::Startup()
                                   kCFRunLoopDefaultMode);
   IOReturn rv = IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone);
   if (rv != kIOReturnSuccess) {
     CFRelease(manager);
     return;
   }
 
   mManager = manager;
+
+  // We held the handle of the CFRunLoop to make sure we
+  // can shut it down explicitly by CFRunLoopStop in another
+  // thread.
+  mMonitorRunLoop = CFRunLoopGetCurrent();
+
+  // CFRunLoopRun() is a blocking message loop when it's called in
+  // non-main thread so this thread cannot receive any other runnables
+  // and nsITimer timeout events after it's called.
+  CFRunLoopRun();
+}
+
+void DarwinGamepadService::Startup()
+{
+  Unused << NS_NewThread(getter_AddRefs(mMonitorThread),
+                         new DarwinGamepadServiceStartupRunnable(this));
 }
 
 void DarwinGamepadService::Shutdown()
 {
   IOHIDManagerRef manager = (IOHIDManagerRef)mManager;
+  CFRunLoopStop(mMonitorRunLoop);
   if (manager) {
     IOHIDManagerClose(manager, 0);
     CFRelease(manager);
     mManager = nullptr;
   }
+  mMonitorThread->Shutdown();
 }
 
 } // namespace
 
 namespace mozilla {
 namespace dom {
 
 DarwinGamepadService* gService = nullptr;
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/ipc/GamepadEventChannelChild.cpp
@@ -0,0 +1,42 @@
+/* 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 "GamepadEventChannelChild.h"
+#include "mozilla/dom/GamepadManager.h"
+
+namespace mozilla {
+namespace dom{
+
+namespace {
+
+class GamepadUpdateRunnable final : public Runnable
+{
+ public:
+  explicit GamepadUpdateRunnable(const GamepadChangeEvent& aGamepadEvent)
+             : mEvent(aGamepadEvent) {}
+  NS_IMETHOD Run() override
+  {
+    RefPtr<GamepadManager> svc(GamepadManager::GetService());
+    if (svc) {
+      svc->Update(mEvent);
+    }
+    return NS_OK;
+  }
+ protected:
+  GamepadChangeEvent mEvent;
+};
+
+} // namespace
+
+bool
+GamepadEventChannelChild::RecvGamepadUpdate(
+                                       const GamepadChangeEvent& aGamepadEvent)
+{
+  nsresult rv;
+  rv = NS_DispatchToMainThread(new GamepadUpdateRunnable(aGamepadEvent));
+  NS_WARN_IF(NS_FAILED(rv));
+  return true;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/ipc/GamepadEventChannelChild.h
@@ -0,0 +1,24 @@
+/* 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 "mozilla/dom/PGamepadEventChannelChild.h"
+
+#ifndef mozilla_dom_GamepadEventChannelChild_h_
+#define mozilla_dom_GamepadEventChannelChild_h_
+
+namespace mozilla{
+namespace dom{
+
+class GamepadEventChannelChild final : public PGamepadEventChannelChild
+{
+ public:
+  GamepadEventChannelChild() {}
+  ~GamepadEventChannelChild() {}
+  virtual bool
+  RecvGamepadUpdate(const GamepadChangeEvent& aGamepadEvent) override;
+};
+
+}// namespace dom
+}// namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/ipc/GamepadEventChannelParent.cpp
@@ -0,0 +1,101 @@
+/* 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 "GamepadEventChannelParent.h"
+#include "GamepadPlatformService.h"
+#include "mozilla/dom/GamepadMonitoring.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+using namespace mozilla::ipc;
+
+namespace {
+
+class SendGamepadUpdateRunnable final : public Runnable
+{
+ private:
+  ~SendGamepadUpdateRunnable() {}
+  RefPtr<GamepadEventChannelParent> mParent;
+  GamepadChangeEvent mEvent;
+ public:
+  SendGamepadUpdateRunnable(GamepadEventChannelParent* aParent,
+                            GamepadChangeEvent aEvent)
+    : mEvent(aEvent)
+  {
+    MOZ_ASSERT(aParent);
+    mParent = aParent;
+  }
+  NS_IMETHOD Run() override
+  {
+    AssertIsOnBackgroundThread();
+    if(mParent->HasGamepadListener()) {
+      Unused << mParent->SendGamepadUpdate(mEvent);
+    }
+    return NS_OK;
+  }
+};
+
+} // namespace
+
+GamepadEventChannelParent::GamepadEventChannelParent()
+  : mHasGamepadListener(false)
+{
+  RefPtr<GamepadPlatformService> service =
+    GamepadPlatformService::GetParentService();
+  MOZ_ASSERT(service);
+  service->AddChannelParent(this);
+  mBackgroundThread = NS_GetCurrentThread();
+}
+
+bool
+GamepadEventChannelParent::RecvGamepadListenerAdded()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mHasGamepadListener);
+  mHasGamepadListener = true;
+  StartGamepadMonitoring();
+  return true;
+}
+
+bool
+GamepadEventChannelParent::RecvGamepadListenerRemoved()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(mHasGamepadListener);
+  mHasGamepadListener = false;
+  RefPtr<GamepadPlatformService> service =
+    GamepadPlatformService::GetParentService();
+  MOZ_ASSERT(service);
+  service->RemoveChannelParent(this);
+  Unused << Send__delete__(this);
+  return true;
+}
+
+void
+GamepadEventChannelParent::ActorDestroy(ActorDestroyReason aWhy)
+{
+  AssertIsOnBackgroundThread();
+
+  // It may be called because IPDL child side crashed, we'll
+  // not receive RecvGamepadListenerRemoved in that case
+  if (mHasGamepadListener) {
+    mHasGamepadListener = false;
+    RefPtr<GamepadPlatformService> service =
+      GamepadPlatformService::GetParentService();
+    MOZ_ASSERT(service);
+    service->RemoveChannelParent(this);
+  }
+  MaybeStopGamepadMonitoring();
+}
+
+void
+GamepadEventChannelParent::DispatchUpdateEvent(const GamepadChangeEvent& aEvent)
+{
+  mBackgroundThread->Dispatch(new SendGamepadUpdateRunnable(this, aEvent),
+                              NS_DISPATCH_NORMAL);
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/ipc/GamepadEventChannelParent.h
@@ -0,0 +1,31 @@
+/* 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 "mozilla/dom/PGamepadEventChannelParent.h"
+
+#ifndef mozilla_dom_GamepadEventChannelParent_h_
+#define mozilla_dom_GamepadEventChannelParent_h_
+
+namespace mozilla{
+namespace dom{
+
+class GamepadEventChannelParent final : public PGamepadEventChannelParent
+{
+ public:
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GamepadEventChannelParent)
+  GamepadEventChannelParent();
+  virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+  virtual bool RecvGamepadListenerAdded() override;
+  virtual bool RecvGamepadListenerRemoved() override;
+  void DispatchUpdateEvent(const GamepadChangeEvent& aEvent);
+  bool HasGamepadListener() const { return mHasGamepadListener; }
+ private:
+  ~GamepadEventChannelParent() {}
+  bool mHasGamepadListener;
+  nsCOMPtr<nsIThread> mBackgroundThread;
+};
+
+}// namespace dom
+}// namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/ipc/GamepadEventTypes.ipdlh
@@ -0,0 +1,41 @@
+/* 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/. */
+
+namespace mozilla {
+namespace dom {
+
+struct GamepadAdded {
+  nsString id;
+  uint32_t index;
+  uint32_t mapping;
+  uint32_t num_buttons;
+  uint32_t num_axes;
+};
+
+struct GamepadRemoved {
+  uint32_t index;
+};
+
+struct GamepadAxisInformation {
+  uint32_t index;
+  uint32_t axis;
+  double value;
+};
+
+struct GamepadButtonInformation {
+  uint32_t index;
+  uint32_t button;
+  bool pressed;
+  double value;
+};
+
+union GamepadChangeEvent {
+  GamepadAdded;
+  GamepadRemoved;
+  GamepadAxisInformation;
+  GamepadButtonInformation;
+};
+
+} // namespace dom
+} // namespace mozilla
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/ipc/GamepadTestChannelChild.cpp
@@ -0,0 +1,32 @@
+/* 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 "GamepadTestChannelChild.h"
+
+namespace mozilla {
+namespace dom {
+
+void
+GamepadTestChannelChild::AddPromise(const uint32_t& aID, Promise* aPromise)
+{
+  MOZ_ASSERT(!mPromiseList.Get(aID, nullptr));
+  mPromiseList.Put(aID, aPromise);
+}
+
+bool
+GamepadTestChannelChild::RecvReplyGamepadIndex(const uint32_t& aID,
+                                               const uint32_t& aIndex)
+{
+  RefPtr<Promise> p;
+  if (!mPromiseList.Get(aID, getter_AddRefs(p))) {
+    MOZ_CRASH("We should always have a promise.");
+  }
+
+  p->MaybeResolve(aIndex);
+  mPromiseList.Remove(aID);
+  return true;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/ipc/GamepadTestChannelChild.h
@@ -0,0 +1,29 @@
+/* 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 "mozilla/dom/PGamepadTestChannelChild.h"
+#include "mozilla/dom/Promise.h"
+
+#ifndef mozilla_dom_GamepadTestChannelChild_h_
+#define mozilla_dom_GamepadTestChannelChild_h_
+
+namespace mozilla {
+namespace dom {
+
+class GamepadTestChannelChild final : public PGamepadTestChannelChild
+{
+ public:
+  GamepadTestChannelChild() {}
+  ~GamepadTestChannelChild() {}
+  void AddPromise(const uint32_t& aID, Promise* aPromise);
+ private:
+  virtual bool RecvReplyGamepadIndex(const uint32_t& aID,
+                                     const uint32_t& aIndex) override;
+  nsRefPtrHashtable<nsUint32HashKey, Promise> mPromiseList;
+};
+
+}// namespace dom
+}// namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/ipc/GamepadTestChannelParent.cpp
@@ -0,0 +1,63 @@
+/* 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 "GamepadTestChannelParent.h"
+
+#include "mozilla/dom/GamepadPlatformService.h"
+#include "mozilla/unused.h"
+
+namespace mozilla {
+namespace dom {
+
+bool
+GamepadTestChannelParent::RecvGamepadTestEvent(const uint32_t& aID,
+                                               const GamepadChangeEvent& aEvent)
+{
+  mozilla::ipc::AssertIsOnBackgroundThread();
+  RefPtr<GamepadPlatformService>  service =
+    GamepadPlatformService::GetParentService();
+  MOZ_ASSERT(service);
+  if (aEvent.type() == GamepadChangeEvent::TGamepadAdded) {
+    const GamepadAdded& a = aEvent.get_GamepadAdded();
+    nsCString gamepadID;
+    LossyCopyUTF16toASCII(a.id(), gamepadID);
+    uint32_t index = service->AddGamepad(gamepadID.get(),
+                                         (GamepadMappingType)a.mapping(),
+                                         a.num_buttons(),
+                                         a.num_axes());
+    if (!mShuttingdown) {
+      Unused << SendReplyGamepadIndex(aID, index);
+    }
+    return true;
+  }
+  if (aEvent.type() == GamepadChangeEvent::TGamepadRemoved) {
+    const GamepadRemoved& a = aEvent.get_GamepadRemoved();
+    service->RemoveGamepad(a.index());
+    return true;
+  }
+  if (aEvent.type() == GamepadChangeEvent::TGamepadButtonInformation) {
+    const GamepadButtonInformation& a = aEvent.get_GamepadButtonInformation();
+    service->NewButtonEvent(a.index(), a.button(), a.pressed(), a.value());
+    return true;
+  }
+  if (aEvent.type() == GamepadChangeEvent::TGamepadAxisInformation) {
+    const GamepadAxisInformation& a = aEvent.get_GamepadAxisInformation();
+    service->NewAxisMoveEvent(a.index(), a.axis(), a.value());
+    return true;
+  }
+
+  NS_WARNING("Unknown event type.");
+  return false;
+}
+
+bool
+GamepadTestChannelParent::RecvShutdownChannel()
+{
+  mShuttingdown = true;
+  Unused << Send__delete__(this);
+  return true;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/ipc/GamepadTestChannelParent.h
@@ -0,0 +1,33 @@
+/* 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 "mozilla/dom/PGamepadTestChannelParent.h"
+
+#ifndef mozilla_dom_GamepadTestChannelParent_h_
+#define mozilla_dom_GamepadTestChannelParent_h_
+
+namespace mozilla {
+namespace dom {
+
+class GamepadTestChannelParent final : public PGamepadTestChannelParent
+{
+ public:
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GamepadTestChannelParent)
+  GamepadTestChannelParent()
+    : mShuttingdown(false) {}
+  virtual void ActorDestroy(ActorDestroyReason aWhy) override {}
+  virtual bool
+  RecvGamepadTestEvent(const uint32_t& aID,
+                       const GamepadChangeEvent& aGamepadEvent) override;
+  virtual bool
+  RecvShutdownChannel() override;
+ private:
+  ~GamepadTestChannelParent() {}
+  bool mShuttingdown;
+};
+
+}// namespace dom
+}// namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/ipc/PGamepadEventChannel.ipdl
@@ -0,0 +1,21 @@
+/* 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 protocol PBackground;
+include GamepadEventTypes;
+
+namespace mozilla {
+namespace dom {
+
+async protocol PGamepadEventChannel {
+  manager PBackground;
+  parent:
+    async GamepadListenerAdded();
+    async GamepadListenerRemoved();
+  child:
+    async __delete__();
+    async GamepadUpdate(GamepadChangeEvent aGamepadEvent);
+};
+
+}
+}
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/ipc/PGamepadTestChannel.ipdl
@@ -0,0 +1,21 @@
+/* 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 protocol PBackground;
+include GamepadEventTypes;
+
+namespace mozilla {
+namespace dom {
+
+async protocol PGamepadTestChannel {
+  manager PBackground;
+  parent:
+    async GamepadTestEvent(uint32_t aID, GamepadChangeEvent aGamepadEvent);
+    async ShutdownChannel();
+  child:
+    async __delete__();
+    async ReplyGamepadIndex(uint32_t aID, uint32_t aIndex);
+};
+
+}
+}
--- a/dom/gamepad/linux/LinuxGamepad.cpp
+++ b/dom/gamepad/linux/LinuxGamepad.cpp
@@ -14,22 +14,22 @@
 
 #include <glib.h>
 #include <linux/joystick.h>
 #include <stdio.h>
 #include <stdint.h>
 #include <sys/ioctl.h>
 #include <unistd.h>
 #include "nscore.h"
-#include "mozilla/dom/GamepadFunctions.h"
+#include "mozilla/dom/GamepadPlatformService.h"
 #include "udev.h"
 
 namespace {
 
-using namespace mozilla::dom::GamepadFunctions;
+using namespace mozilla::dom;
 using mozilla::udev_lib;
 using mozilla::udev_device;
 using mozilla::udev_list_entry;
 using mozilla::udev_enumerate;
 using mozilla::udev_monitor;
 
 static const float kMaxAxisValue = 32767.0;
 static const char kJoystickPath[] = "/dev/input/js";
@@ -81,16 +81,22 @@ private:
 };
 
 // singleton instance
 LinuxGamepadService* gService = nullptr;
 
 void
 LinuxGamepadService::AddDevice(struct udev_device* dev)
 {
+  RefPtr<GamepadPlatformService> service =
+    GamepadPlatformService::GetParentService();
+  if (!service) {
+    return;
+  }
+
   const char* devpath = mUdev.udev_device_get_devnode(dev);
   if (!devpath) {
     return;
   }
 
   // Ensure that this device hasn't already been added.
   for (unsigned int i = 0; i < mGamepads.Length(); i++) {
     if (strcmp(mGamepads[i].devpath, devpath) == 0) {
@@ -134,43 +140,49 @@ LinuxGamepadService::AddDevice(struct ud
            name);
 
   char numAxes = 0, numButtons = 0;
   ioctl(fd, JSIOCGAXES, &numAxes);
   gamepad.numAxes = numAxes;
   ioctl(fd, JSIOCGBUTTONS, &numButtons);
   gamepad.numButtons = numButtons;
 
-  gamepad.index = AddGamepad(gamepad.idstring,
-                             mozilla::dom::GamepadMappingType::_empty,
-                             gamepad.numButtons,
-                             gamepad.numAxes);
+  gamepad.index = service->AddGamepad(gamepad.idstring,
+                                      mozilla::dom::GamepadMappingType::_empty,
+                                      gamepad.numButtons,
+                                      gamepad.numAxes);
 
   gamepad.source_id =
     g_io_add_watch(channel,
                    GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP),
                    OnGamepadData,
                    GINT_TO_POINTER(gamepad.index));
   g_io_channel_unref(channel);
 
   mGamepads.AppendElement(gamepad);
 }
 
 void
 LinuxGamepadService::RemoveDevice(struct udev_device* dev)
 {
+  RefPtr<GamepadPlatformService> service =
+    GamepadPlatformService::GetParentService();
+  if (!service) {
+    return;
+  }
+
   const char* devpath = mUdev.udev_device_get_devnode(dev);
   if (!devpath) {
     return;
   }
 
   for (unsigned int i = 0; i < mGamepads.Length(); i++) {
     if (strcmp(mGamepads[i].devpath, devpath) == 0) {
       g_source_remove(mGamepads[i].source_id);
-      RemoveGamepad(mGamepads[i].index);
+      service->RemoveGamepad(mGamepads[i].index);
       mGamepads.RemoveElementAt(i);
       break;
     }
   }
 }
 
 void
 LinuxGamepadService::ScanForDevices()
@@ -290,16 +302,21 @@ LinuxGamepadService::ReadUdevChange()
 }
 
 // static
 gboolean
 LinuxGamepadService::OnGamepadData(GIOChannel* source,
                                    GIOCondition condition,
                                    gpointer data)
 {
+  RefPtr<GamepadPlatformService> service =
+    GamepadPlatformService::GetParentService();
+  if (!service) {
+    return TRUE;
+  }
   int index = GPOINTER_TO_INT(data);
   //TODO: remove gamepad?
   if (condition & G_IO_ERR || condition & G_IO_HUP)
     return FALSE;
 
   while (true) {
     struct js_event event;
     gsize count;
@@ -315,21 +332,21 @@ LinuxGamepadService::OnGamepadData(GIOCh
 
     //TODO: store device state?
     if (event.type & JS_EVENT_INIT) {
       continue;
     }
 
     switch (event.type) {
     case JS_EVENT_BUTTON:
-      NewButtonEvent(index, event.number, !!event.value);
+      service->NewButtonEvent(index, event.number, !!event.value);
       break;
     case JS_EVENT_AXIS:
-      NewAxisMoveEvent(index, event.number,
-                       ((float)event.value) / kMaxAxisValue);
+      service->NewAxisMoveEvent(index, event.number,
+                                ((float)event.value) / kMaxAxisValue);
       break;
     }
   }
 
   return TRUE;
 }
 
 // static
--- a/dom/gamepad/moz.build
+++ b/dom/gamepad/moz.build
@@ -2,29 +2,37 @@
 # 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/.
 
 EXPORTS.mozilla.dom += [
     'Gamepad.h',
     'GamepadButton.h',
-    'GamepadFunctions.h',
+    'GamepadManager.h',
     'GamepadMonitoring.h',
-    'GamepadService.h',
-    'GamepadServiceTest.h'
+    'GamepadPlatformService.h',
+    'GamepadServiceTest.h',
+    'ipc/GamepadEventChannelChild.h',
+    'ipc/GamepadEventChannelParent.h',
+    'ipc/GamepadTestChannelChild.h',
+    'ipc/GamepadTestChannelParent.h'
     ]
 
 UNIFIED_SOURCES = [
     'Gamepad.cpp',
     'GamepadButton.cpp',
-    'GamepadFunctions.cpp',
+    'GamepadManager.cpp',
     'GamepadMonitoring.cpp',
-    'GamepadService.cpp',
-    'GamepadServiceTest.cpp'
+    'GamepadPlatformService.cpp',
+    'GamepadServiceTest.cpp',
+    'ipc/GamepadEventChannelChild.cpp',
+    'ipc/GamepadEventChannelParent.cpp',
+    'ipc/GamepadTestChannelChild.cpp',
+    'ipc/GamepadTestChannelParent.cpp'
     ]
 
 if CONFIG['MOZ_GAMEPAD_BACKEND'] == 'stub':
     UNIFIED_SOURCES += [
         'fallback/FallbackGamepad.cpp'
     ]
 elif CONFIG['MOZ_GAMEPAD_BACKEND'] == 'cocoa':
     UNIFIED_SOURCES += [
@@ -38,16 +46,26 @@ elif CONFIG['MOZ_GAMEPAD_BACKEND'] == 'l
     UNIFIED_SOURCES += [
         'linux/LinuxGamepad.cpp'
     ]
 elif CONFIG['MOZ_GAMEPAD_BACKEND'] == 'android':
     UNIFIED_SOURCES += [
         'android/AndroidGamepad.cpp'
     ]
 
+LOCAL_INCLUDES += [
+    'ipc',
+]
+
+IPDL_SOURCES += [
+    'ipc/GamepadEventTypes.ipdlh',
+    'ipc/PGamepadEventChannel.ipdl',
+    'ipc/PGamepadTestChannel.ipdl'
+]
+
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul'
 LOCAL_INCLUDES += [
     '/dom/base',
 ]
 
 CFLAGS += CONFIG['GLIB_CFLAGS']
--- a/dom/gamepad/windows/WindowsGamepad.cpp
+++ b/dom/gamepad/windows/WindowsGamepad.cpp
@@ -11,28 +11,30 @@
 #define UNICODE
 #endif
 #include <windows.h>
 #include <hidsdi.h>
 #include <stdio.h>
 #include <xinput.h>
 
 #include "nsIComponentManager.h"
-#include "nsIObserver.h"
-#include "nsIObserverService.h"
 #include "nsITimer.h"
 #include "nsTArray.h"
+#include "nsThreadUtils.h"
+
 #include "mozilla/ArrayUtils.h"
-#include "mozilla/dom/GamepadFunctions.h"
 #include "mozilla/Services.h"
 
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/dom/GamepadPlatformService.h"
+
 namespace {
 
+using namespace mozilla;
 using namespace mozilla::dom;
-using namespace mozilla::dom::GamepadFunctions;
 using mozilla::ArrayLength;
 
 // USB HID usage tables, page 1 (Hat switch)
 const unsigned kUsageDpad = 0x39;
 // USB HID usage tables, page 1, 0x30 = X
 const unsigned kFirstAxis = 0x30;
 
 // USB HID usage tables
@@ -90,17 +92,22 @@ const size_t kNumMappings = ArrayLength(
 
 enum GamepadType {
   kNoGamepad = 0,
   kRawInputGamepad,
   kXInputGamepad
 };
 
 class WindowsGamepadService;
-WindowsGamepadService* gService = nullptr;
+// This pointer holds a windows gamepad backend service,
+// it will be created and destroyed by background thread and
+// used by gMonitorThread
+WindowsGamepadService* MOZ_NON_OWNING_REF gService = nullptr;
+nsCOMPtr<nsIThread> gMonitorThread = nullptr;
+static bool sIsShutdown = false;
 
 struct Gamepad {
   GamepadType type;
 
   // Handle to raw input device
   HANDLE handle;
 
   // XInput Index of the user's controller. Passed to XInputGetState.
@@ -250,70 +257,16 @@ SupportedUsage(USHORT page, USHORT usage
   for (unsigned i = 0; i < ArrayLength(kUsagePages); i++) {
     if (page == kUsagePages[i].usagePage && usage == kUsagePages[i].usage) {
       return true;
     }
   }
   return false;
 }
 
-class Observer : public nsIObserver {
-public:
-  NS_DECL_ISUPPORTS
-  NS_DECL_NSIOBSERVER
-
-  Observer(WindowsGamepadService& svc) : mSvc(svc),
-                                         mObserving(true)
-  {
-    nsresult rv;
-    mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
-    nsCOMPtr<nsIObserverService> observerService =
-      mozilla::services::GetObserverService();
-    observerService->AddObserver(this,
-                                 NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID,
-                                 false);
-  }
-
-  void Stop()
-  {
-    if (mTimer) {
-      mTimer->Cancel();
-    }
-    if (mObserving) {
-      nsCOMPtr<nsIObserverService> observerService =
-        mozilla::services::GetObserverService();
-      observerService->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID);
-      mObserving = false;
-    }
-  }
-
-  void SetDeviceChangeTimer()
-  {
-    // Set stable timer, since we will get multiple devices-changed
-    // notifications at once
-    if (mTimer) {
-      mTimer->Cancel();
-      mTimer->Init(this, kDevicesChangedStableDelay, nsITimer::TYPE_ONE_SHOT);
-    }
-  }
-
-private:
-  virtual ~Observer()
-  {
-    Stop();
-  }
-
-  // Gamepad service owns us, we just hold a reference back to it.
-  WindowsGamepadService& mSvc;
-  nsCOMPtr<nsITimer> mTimer;
-  bool mObserving;
-};
-
-NS_IMPL_ISUPPORTS(Observer, nsIObserver);
-
 class HIDLoader {
 public:
   HIDLoader() : mModule(LoadLibraryW(L"hid.dll")),
                 mHidD_GetProductString(nullptr),
                 mHidP_GetCaps(nullptr),
                 mHidP_GetButtonCaps(nullptr),
                 mHidP_GetValueCaps(nullptr),
                 mHidP_GetUsages(nullptr),
@@ -355,69 +308,65 @@ public:
   decltype(HidP_GetUsages) *mHidP_GetUsages;
   decltype(HidP_GetUsageValue) *mHidP_GetUsageValue;
   decltype(HidP_GetScaledUsageValue) *mHidP_GetScaledUsageValue;
 
 private:
   HMODULE mModule;
 };
 
-class WindowsGamepadService {
-public:
-  WindowsGamepadService();
+class WindowsGamepadService
+{
+ public:
+  WindowsGamepadService()
+  {
+    mXInputTimer = do_CreateInstance("@mozilla.org/timer;1");
+    mDeviceChangeTimer = do_CreateInstance("@mozilla.org/timer;1");
+  }
   virtual ~WindowsGamepadService()
   {
     Cleanup();
   }
 
-  enum DeviceChangeType {
-    DeviceChangeNotification,
-    DeviceChangeStable
-  };
-  void DevicesChanged(DeviceChangeType type);
+  void DevicesChanged(bool aIsStablizing);
   void Startup();
   void Shutdown();
   // Parse gamepad input from a WM_INPUT message.
   bool HandleRawInput(HRAWINPUT handle);
 
-private:
+  static void XInputMessageLoopOnceCallback(nsITimer *aTimer, void* aClosure);
+  static void DevicesChangeCallback(nsITimer *aTimer, void* aService);
+
+ private:
   void ScanForDevices();
   // Look for connected raw input devices.
   void ScanForRawInputDevices();
   // Look for connected XInput devices.
   bool ScanForXInputDevices();
   bool HaveXInputGamepad(int userIndex);
 
-  // Timer callback for XInput polling
-  static void XInputPollTimerCallback(nsITimer* aTimer, void* aClosure);
+  bool mIsXInputMonitoring;
   void PollXInput();
   void CheckXInputChanges(Gamepad& gamepad, XINPUT_STATE& state);
 
   // Get information about a raw input gamepad.
   bool GetRawGamepad(HANDLE handle);
   void Cleanup();
 
   // List of connected devices.
   nsTArray<Gamepad> mGamepads;
 
-  RefPtr<Observer> mObserver;
-  nsCOMPtr<nsITimer> mXInputPollTimer;
-
   HIDLoader mHID;
   XInputLoader mXInput;
+
+  nsCOMPtr<nsITimer> mXInputTimer;
+  nsCOMPtr<nsITimer> mDeviceChangeTimer;
 };
 
 
-WindowsGamepadService::WindowsGamepadService()
-{
-  nsresult rv;
-  mXInputPollTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
-  mObserver = new Observer(*this);
-}
-
 void
 WindowsGamepadService::ScanForRawInputDevices()
 {
   if (!mHID) {
     return;
   }
 
   UINT numDevices;
@@ -434,16 +383,40 @@ WindowsGamepadService::ScanForRawInputDe
 
   for (unsigned i = 0; i < devices.Length(); i++) {
     if (devices[i].dwType == RIM_TYPEHID) {
       GetRawGamepad(devices[i].hDevice);
     }
   }
 }
 
+// static
+void
+WindowsGamepadService::XInputMessageLoopOnceCallback(nsITimer *aTimer,
+                                                     void* aService)
+{
+  MOZ_ASSERT(aService);
+  WindowsGamepadService* self = static_cast<WindowsGamepadService*>(aService);
+  self->PollXInput();
+  if (self->mIsXInputMonitoring) {
+    aTimer->Cancel();
+    aTimer->InitWithFuncCallback(XInputMessageLoopOnceCallback, self,
+                                 kXInputPollInterval, nsITimer::TYPE_ONE_SHOT);
+  }
+}
+
+// static
+void
+WindowsGamepadService::DevicesChangeCallback(nsITimer *aTimer, void* aService)
+{
+  MOZ_ASSERT(aService);
+  WindowsGamepadService* self = static_cast<WindowsGamepadService*>(aService);
+  self->DevicesChanged(false);
+}
+
 bool
 WindowsGamepadService::HaveXInputGamepad(int userIndex)
 {
   for (unsigned int i = 0; i < mGamepads.Length(); i++) {
     if (mGamepads[i].type == kXInputGamepad
         && mGamepads[i].userIndex == userIndex) {
       mGamepads[i].present = true;
       return true;
@@ -453,16 +426,22 @@ WindowsGamepadService::HaveXInputGamepad
 }
 
 bool
 WindowsGamepadService::ScanForXInputDevices()
 {
   MOZ_ASSERT(mXInput, "XInput should be present!");
 
   bool found = false;
+  RefPtr<GamepadPlatformService> service =
+    GamepadPlatformService::GetParentService();
+  if (!service) {
+    return found;
+  }
+
   for (int i = 0; i < XUSER_MAX_COUNT; i++) {
     XINPUT_STATE state = {};
     if (mXInput.mXInputGetState(i, &state) != ERROR_SUCCESS) {
       continue;
     }
     found = true;
     // See if this device is already present in our list.
     if (HaveXInputGamepad(i)) {
@@ -472,65 +451,63 @@ WindowsGamepadService::ScanForXInputDevi
     // Not already present, add it.
     Gamepad gamepad = {};
     gamepad.type = kXInputGamepad;
     gamepad.present = true;
     gamepad.state = state;
     gamepad.userIndex = i;
     gamepad.numButtons = kStandardGamepadButtons;
     gamepad.numAxes = kStandardGamepadAxes;
-    gamepad.id = AddGamepad("xinput",
-                            GamepadMappingType::Standard,
-                            kStandardGamepadButtons,
-                            kStandardGamepadAxes);
+    gamepad.id = service->AddGamepad("xinput",
+                                     GamepadMappingType::Standard,
+                                     kStandardGamepadButtons,
+                                     kStandardGamepadAxes);
     mGamepads.AppendElement(gamepad);
   }
 
   return found;
 }
 
 void
 WindowsGamepadService::ScanForDevices()
 {
+  RefPtr<GamepadPlatformService> service =
+    GamepadPlatformService::GetParentService();
+  if (!service) {
+    return;
+  }
+
   for (int i = mGamepads.Length() - 1; i >= 0; i--) {
     mGamepads[i].present = false;
   }
 
   if (mHID) {
     ScanForRawInputDevices();
   }
   if (mXInput) {
-    mXInputPollTimer->Cancel();
+    mXInputTimer->Cancel();
     if (ScanForXInputDevices()) {
-      mXInputPollTimer->InitWithFuncCallback(XInputPollTimerCallback,
-                                             this,
-                                             kXInputPollInterval,
-                                             nsITimer::TYPE_REPEATING_SLACK);
+      mIsXInputMonitoring = true;
+      mXInputTimer->InitWithFuncCallback(XInputMessageLoopOnceCallback, this,
+                                         kXInputPollInterval,
+                                         nsITimer::TYPE_ONE_SHOT);
+    } else {
+      mIsXInputMonitoring = false;
     }
   }
 
   // Look for devices that are no longer present and remove them.
   for (int i = mGamepads.Length() - 1; i >= 0; i--) {
     if (!mGamepads[i].present) {
-      RemoveGamepad(mGamepads[i].id);
+      service->RemoveGamepad(mGamepads[i].id);
       mGamepads.RemoveElementAt(i);
     }
   }
 }
 
-// static
-void
-WindowsGamepadService::XInputPollTimerCallback(nsITimer* aTimer,
-                                               void* aClosure)
-{
-  WindowsGamepadService* self =
-    reinterpret_cast<WindowsGamepadService*>(aClosure);
-  self->PollXInput();
-}
-
 void
 WindowsGamepadService::PollXInput()
 {
   for (unsigned int i = 0; i < mGamepads.Length(); i++) {
     if (mGamepads[i].type != kXInputGamepad) {
       continue;
     }
 
@@ -540,60 +517,65 @@ WindowsGamepadService::PollXInput()
         && state.dwPacketNumber != mGamepads[i].state.dwPacketNumber) {
         CheckXInputChanges(mGamepads[i], state);
     }
   }
 }
 
 void WindowsGamepadService::CheckXInputChanges(Gamepad& gamepad,
                                                XINPUT_STATE& state) {
+  RefPtr<GamepadPlatformService> service =
+    GamepadPlatformService::GetParentService();
+  if (!service) {
+    return;
+  }
   // Handle digital buttons first
   for (size_t b = 0; b < kNumMappings; b++) {
     if (state.Gamepad.wButtons & kXIButtonMap[b].button &&
         !(gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button)) {
       // Button pressed
-      NewButtonEvent(gamepad.id, kXIButtonMap[b].mapped, true);
+      service->NewButtonEvent(gamepad.id, kXIButtonMap[b].mapped, true);
     } else if (!(state.Gamepad.wButtons & kXIButtonMap[b].button) &&
                gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button) {
       // Button released
-      NewButtonEvent(gamepad.id, kXIButtonMap[b].mapped, false);
+      service->NewButtonEvent(gamepad.id, kXIButtonMap[b].mapped, false);
     }
   }
 
   // Then triggers
   if (state.Gamepad.bLeftTrigger != gamepad.state.Gamepad.bLeftTrigger) {
     bool pressed =
       state.Gamepad.bLeftTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
-    NewButtonEvent(gamepad.id, kButtonLeftTrigger,
-                   pressed, state.Gamepad.bLeftTrigger / 255.0);
+    service->NewButtonEvent(gamepad.id, kButtonLeftTrigger,
+                            pressed, state.Gamepad.bLeftTrigger / 255.0);
   }
   if (state.Gamepad.bRightTrigger != gamepad.state.Gamepad.bRightTrigger) {
     bool pressed =
       state.Gamepad.bRightTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
-    NewButtonEvent(gamepad.id, kButtonRightTrigger,
-                   pressed, state.Gamepad.bRightTrigger / 255.0);
+    service->NewButtonEvent(gamepad.id, kButtonRightTrigger,
+                            pressed, state.Gamepad.bRightTrigger / 255.0);
   }
 
   // Finally deal with analog sticks
   // TODO: bug 1001955 - Support deadzones.
   if (state.Gamepad.sThumbLX != gamepad.state.Gamepad.sThumbLX) {
-    NewAxisMoveEvent(gamepad.id, kLeftStickXAxis,
-                     state.Gamepad.sThumbLX / 32767.0);
+    service->NewAxisMoveEvent(gamepad.id, kLeftStickXAxis,
+                              state.Gamepad.sThumbLX / 32767.0);
   }
   if (state.Gamepad.sThumbLY != gamepad.state.Gamepad.sThumbLY) {
-    NewAxisMoveEvent(gamepad.id, kLeftStickYAxis,
-                     -1.0 * state.Gamepad.sThumbLY / 32767.0);
+    service->NewAxisMoveEvent(gamepad.id, kLeftStickYAxis,
+                              -1.0 * state.Gamepad.sThumbLY / 32767.0);
   }
   if (state.Gamepad.sThumbRX != gamepad.state.Gamepad.sThumbRX) {
-    NewAxisMoveEvent(gamepad.id, kRightStickXAxis,
-                     state.Gamepad.sThumbRX / 32767.0);
+    service->NewAxisMoveEvent(gamepad.id, kRightStickXAxis,
+                              state.Gamepad.sThumbRX / 32767.0);
   }
   if (state.Gamepad.sThumbRY != gamepad.state.Gamepad.sThumbRY) {
-    NewAxisMoveEvent(gamepad.id, kRightStickYAxis,
-                     -1.0 * state.Gamepad.sThumbRY / 32767.0);
+    service->NewAxisMoveEvent(gamepad.id, kRightStickYAxis,
+                              -1.0 * state.Gamepad.sThumbRY / 32767.0);
   }
   gamepad.state = state;
 }
 
 // Used to sort a list of axes by HID usage.
 class HidValueComparator {
 public:
   bool Equals(const HIDP_VALUE_CAPS& c1, const HIDP_VALUE_CAPS& c2) const
@@ -607,16 +589,22 @@ public:
     }
     return c1.UsagePage < c2.UsagePage;
   }
 };
 
 bool
 WindowsGamepadService::GetRawGamepad(HANDLE handle)
 {
+  RefPtr<GamepadPlatformService> service =
+    GamepadPlatformService::GetParentService();
+  if (!service) {
+    return true;
+  }
+
   if (!mHID) {
     return false;
   }
 
   for (unsigned i = 0; i < mGamepads.Length(); i++) {
     if (mGamepads[i].type == kRawInputGamepad && mGamepads[i].handle == handle) {
       mGamepads[i].present = true;
       return true;
@@ -743,32 +731,37 @@ WindowsGamepadService::GetRawGamepad(HAN
     if (i >= kMaxAxes) {
       break;
     }
     gamepad.axes[i].caps = axes[i];
   }
   gamepad.type = kRawInputGamepad;
   gamepad.handle = handle;
   gamepad.present = true;
-
-  gamepad.id = GamepadFunctions::AddGamepad(gamepad_id,
-                                            GamepadMappingType::_empty,
-                                            gamepad.numButtons,
-                                            gamepad.numAxes);
+  gamepad.id = service->AddGamepad(gamepad_id,
+                                   GamepadMappingType::_empty,
+                                   gamepad.numButtons,
+                                   gamepad.numAxes);
   mGamepads.AppendElement(gamepad);
   return true;
 }
 
 bool
 WindowsGamepadService::HandleRawInput(HRAWINPUT handle)
 {
   if (!mHID) {
     return false;
   }
 
+  RefPtr<GamepadPlatformService> service =
+    GamepadPlatformService::GetParentService();
+  if (service) {
+    return true;
+  }
+
   // First, get data from the handle
   UINT size;
   GetRawInputData(handle, RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER));
   nsTArray<uint8_t> data(size);
   data.SetLength(size);
   if (GetRawInputData(handle, RID_INPUT, data.Elements(), &size,
                       sizeof(RAWINPUTHEADER)) == kRawInputError) {
     return false;
@@ -816,17 +809,17 @@ WindowsGamepadService::HandleRawInput(HR
     ULONG value;
     if (mHID.mHidP_GetUsageValue(HidP_Input, gamepad->dpadCaps.UsagePage, 0, gamepad->dpadCaps.Range.UsageMin, &value, parsed, (PCHAR)raw->data.hid.bRawData, raw->data.hid.dwSizeHid) == HIDP_STATUS_SUCCESS) {
       UnpackDpad(static_cast<LONG>(value), gamepad, buttons);
     }
   }
 
   for (unsigned i = 0; i < gamepad->numButtons; i++) {
     if (gamepad->buttons[i] != buttons[i]) {
-      NewButtonEvent(gamepad->id, i, buttons[i]);
+      service->NewButtonEvent(gamepad->id, i, buttons[i]);
       gamepad->buttons[i] = buttons[i];
     }
   }
 
   // Get all axis values.
   for (unsigned i = 0; i < gamepad->numAxes; i++) {
     double new_value;
     if (gamepad->axes[i].caps.LogicalMin < 0) {
@@ -836,31 +829,30 @@ LONG value;
                                    &value, parsed,
                                    (PCHAR)raw->data.hid.bRawData,
                                    raw->data.hid.dwSizeHid)
           != HIDP_STATUS_SUCCESS) {
         continue;
       }
       new_value = ScaleAxis(value, gamepad->axes[i].caps.LogicalMin,
                             gamepad->axes[i].caps.LogicalMax);
-    }
-    else {
+    } else {
       ULONG value;
       if (mHID.mHidP_GetUsageValue(HidP_Input, gamepad->axes[i].caps.UsagePage, 0,
                              gamepad->axes[i].caps.Range.UsageMin, &value,
                              parsed, (PCHAR)raw->data.hid.bRawData,
                              raw->data.hid.dwSizeHid) != HIDP_STATUS_SUCCESS) {
         continue;
       }
 
       new_value = ScaleAxis(value, gamepad->axes[i].caps.LogicalMin,
                             gamepad->axes[i].caps.LogicalMax);
     }
     if (gamepad->axes[i].value != new_value) {
-      NewAxisMoveEvent(gamepad->id, i, new_value);
+      service->NewAxisMoveEvent(gamepad->id, i, new_value);
       gamepad->axes[i].value = new_value;
     }
   }
 
   return true;
 }
 
 void
@@ -873,51 +865,39 @@ void
 WindowsGamepadService::Shutdown()
 {
   Cleanup();
 }
 
 void
 WindowsGamepadService::Cleanup()
 {
-  if (mXInputPollTimer) {
-    mXInputPollTimer->Cancel();
+  mIsXInputMonitoring = false;
+  if (mXInputTimer) {
+    mXInputTimer->Cancel();
+  }
+  if (mDeviceChangeTimer) {
+    mDeviceChangeTimer->Cancel();
   }
   mGamepads.Clear();
-  if (mObserver) {
-    mObserver->Stop();
-    mObserver = nullptr;
-  }
 }
 
 void
-WindowsGamepadService::DevicesChanged(DeviceChangeType type)
+WindowsGamepadService::DevicesChanged(bool aIsStablizing)
 {
-  if (type == DeviceChangeNotification) {
-    if (mObserver) {
-      mObserver->SetDeviceChangeTimer();
-    }
-  } else if (type == DeviceChangeStable) {
+  if (aIsStablizing) {
+    mDeviceChangeTimer->Cancel();
+    mDeviceChangeTimer->InitWithFuncCallback(DevicesChangeCallback, this,
+                                             kDevicesChangedStableDelay,
+                                             nsITimer::TYPE_ONE_SHOT);
+  } else {
     ScanForDevices();
   }
 }
 
-NS_IMETHODIMP
-Observer::Observe(nsISupports* aSubject,
-                  const char* aTopic,
-                  const char16_t* aData)
-{
-  if (strcmp(aTopic, "timer-callback") == 0) {
-    mSvc.DevicesChanged(WindowsGamepadService::DeviceChangeStable);
-  } else if (strcmp(aTopic, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID) == 0) {
-    Stop();
-  }
-  return NS_OK;
-}
-
 HWND sHWnd = nullptr;
 
 bool
 RegisterRawInput(HWND hwnd, bool enable)
 {
   nsTArray<RAWINPUTDEVICE> rid(ArrayLength(kUsagePages));
   rid.SetLength(ArrayLength(kUsagePages));
 
@@ -945,74 +925,141 @@ GamepadWindowProc(HWND hwnd, UINT msg, W
   const unsigned int DBT_DEVNODES_CHANGED     = 0x7;
 
   switch (msg) {
   case WM_DEVICECHANGE:
     if (wParam == DBT_DEVICEARRIVAL ||
         wParam == DBT_DEVICEREMOVECOMPLETE ||
         wParam == DBT_DEVNODES_CHANGED) {
       if (gService) {
-        gService->DevicesChanged(WindowsGamepadService::DeviceChangeNotification);
+        gService->DevicesChanged(true);
       }
     }
     break;
   case WM_INPUT:
     if (gService) {
       gService->HandleRawInput(reinterpret_cast<HRAWINPUT>(lParam));
     }
     break;
   }
   return DefWindowProc(hwnd, msg, wParam, lParam);
 }
 
+class WindowGamepadMessageLoopOnceRunnable final : public Runnable
+{
+public:
+  WindowGamepadMessageLoopOnceRunnable() {}
+  NS_IMETHOD Run() override
+  {
+    MOZ_ASSERT(NS_GetCurrentThread() == gMonitorThread);
+    MSG msg;
+    while (PeekMessageW(&msg, sHWnd, 0, 0, PM_REMOVE) > 0) {
+      TranslateMessage(&msg);
+      DispatchMessage(&msg);
+    }
+    if (!sIsShutdown) {
+      NS_DispatchToCurrentThread(new WindowGamepadMessageLoopOnceRunnable());
+    }
+    return NS_OK;
+  }
+private:
+  ~WindowGamepadMessageLoopOnceRunnable() {}
+};
+
+class StartWindowsGamepadServiceRunnable final : public Runnable
+{
+public:
+  StartWindowsGamepadServiceRunnable() {}
+
+  NS_IMETHOD Run() override
+  {
+    MOZ_ASSERT(NS_GetCurrentThread() == gMonitorThread);
+    gService = new WindowsGamepadService();
+    gService->Startup();
+
+    if (sHWnd == nullptr) {
+      WNDCLASSW wc;
+      HMODULE hSelf = GetModuleHandle(nullptr);
+
+      if (!GetClassInfoW(hSelf, L"MozillaGamepadClass", &wc)) {
+        ZeroMemory(&wc, sizeof(WNDCLASSW));
+        wc.hInstance = hSelf;
+        wc.lpfnWndProc = GamepadWindowProc;
+        wc.lpszClassName = L"MozillaGamepadClass";
+        RegisterClassW(&wc);
+      }
+
+      sHWnd = CreateWindowW(L"MozillaGamepadClass", L"Gamepad Watcher",
+        0, 0, 0, 0, 0,
+        nullptr, nullptr, hSelf, nullptr);
+      RegisterRawInput(sHWnd, true);
+    }
+
+    // Explicitly start the message loop
+    NS_DispatchToCurrentThread(new WindowGamepadMessageLoopOnceRunnable());
+
+    return NS_OK;
+  }
+private:
+  ~StartWindowsGamepadServiceRunnable() {}
+};
+
+class StopWindowsGamepadServiceRunnable final : public Runnable
+{
+ public:
+  StopWindowsGamepadServiceRunnable() {}
+
+  NS_IMETHOD Run() override
+  {
+    MOZ_ASSERT(NS_GetCurrentThread() == gMonitorThread);
+    if (sHWnd) {
+      RegisterRawInput(sHWnd, false);
+      DestroyWindow(sHWnd);
+      sHWnd = nullptr;
+    }
+
+    gService->Shutdown();
+    delete gService;
+    gService = nullptr;
+
+    return NS_OK;
+  }
+ private:
+  ~StopWindowsGamepadServiceRunnable() {}
+};
+
 } // namespace
 
 namespace mozilla {
 namespace dom {
 
-void StartGamepadMonitoring()
+using namespace mozilla::ipc;
+
+void
+StartGamepadMonitoring()
 {
-  if (gService) {
+  AssertIsOnBackgroundThread();
+
+  if (gMonitorThread || gService) {
     return;
   }
-
-  gService = new WindowsGamepadService();
-  gService->Startup();
-
-  if (sHWnd == nullptr) {
-    WNDCLASSW wc;
-    HMODULE hSelf = GetModuleHandle(nullptr);
-
-    if (!GetClassInfoW(hSelf, L"MozillaGamepadClass", &wc)) {
-      ZeroMemory(&wc, sizeof(WNDCLASSW));
-      wc.hInstance = hSelf;
-      wc.lpfnWndProc = GamepadWindowProc;
-      wc.lpszClassName = L"MozillaGamepadClass";
-      RegisterClassW(&wc);
-    }
-
-    sHWnd = CreateWindowW(L"MozillaGamepadClass", L"Gamepad Watcher",
-                          0, 0, 0, 0, 0,
-                          nullptr, nullptr, hSelf, nullptr);
-    RegisterRawInput(sHWnd, true);
-  }
+  sIsShutdown = false;
+  NS_NewThread(getter_AddRefs(gMonitorThread));
+  gMonitorThread->Dispatch(new StartWindowsGamepadServiceRunnable(),
+                           NS_DISPATCH_NORMAL);
 }
 
-void StopGamepadMonitoring()
+void
+StopGamepadMonitoring()
 {
-  if (!gService) {
+  AssertIsOnBackgroundThread();
+
+  if (sIsShutdown) {
     return;
   }
-
-  if (sHWnd) {
-    RegisterRawInput(sHWnd, false);
-    DestroyWindow(sHWnd);
-    sHWnd = nullptr;
-  }
-
-  gService->Shutdown();
-  delete gService;
-  gService = nullptr;
+  sIsShutdown = true;
+  gMonitorThread->Dispatch(new StopWindowsGamepadServiceRunnable(), NS_DISPATCH_NORMAL);
+  gMonitorThread->Shutdown();
+  gMonitorThread = nullptr;
 }
 
 } // namespace dom
 } // namespace mozilla
-
deleted file mode 100644
--- a/dom/interfaces/gamepad/moz.build
+++ /dev/null
@@ -1,11 +0,0 @@
-# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
-# 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/.
-
-XPIDL_MODULE = 'dom_gamepad'
-
-XPIDL_SOURCES = [
-  'nsIGamepadServiceTest.idl',
-  ]
deleted file mode 100644
--- a/dom/interfaces/gamepad/nsIGamepadServiceTest.idl
+++ /dev/null
@@ -1,29 +0,0 @@
-/* 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"
-
-interface nsIVariant;
-
-/*
- * This interface is intended only for use in tests.
- */
-[scriptable, uuid(c03ec4ed-8a7e-40e7-99da-c609f1760d0c)]
-interface nsIGamepadServiceTest : nsISupports
-{
-  const unsigned long NO_MAPPING = 0;
-  const unsigned long STANDARD_MAPPING = 1;
-
-  unsigned long addGamepad(in string id,
-                           in unsigned long mapping,
-                           in unsigned long numButtons,
-			   in unsigned long numAxes);
-  void removeGamepad(in unsigned long index);
-  void newButtonEvent(in unsigned long index, in unsigned long button,
-		      in boolean pressed);
-  void newButtonValueEvent(in unsigned long index, in unsigned long button,
-                           in boolean pressed, in double value);
-  void newAxisMoveEvent(in unsigned long index, in unsigned long axis,
-			in double value);
-};
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -168,20 +168,16 @@
 #include "nsIAccessibilityService.h"
 #endif
 
 #ifdef MOZ_NUWA_PROCESS
 #include "ipc/Nuwa.h"
 #endif
 #include "NuwaChild.h"
 
-#ifdef MOZ_GAMEPAD
-#include "mozilla/dom/GamepadService.h"
-#endif
-
 #ifndef MOZ_SIMPLEPUSH
 #include "mozilla/dom/PushNotifier.h"
 #endif
 
 #include "mozilla/dom/File.h"
 #include "mozilla/dom/cellbroadcast/CellBroadcastIPCService.h"
 #include "mozilla/dom/icc/IccChild.h"
 #include "mozilla/dom/mobileconnection/MobileConnectionChild.h"
@@ -3225,28 +3221,16 @@ ContentChild::RecvPWebBrowserPersistDocu
 bool
 ContentChild::DeallocPWebBrowserPersistDocumentChild(PWebBrowserPersistDocumentChild* aActor)
 {
   delete aActor;
   return true;
 }
 
 bool
-ContentChild::RecvGamepadUpdate(const GamepadChangeEvent& aGamepadEvent)
-{
-#ifdef MOZ_GAMEPAD
-  RefPtr<GamepadService> svc(GamepadService::GetService());
-  if (svc) {
-    svc->Update(aGamepadEvent);
-  }
-#endif
-  return true;
-}
-
-bool
 ContentChild::RecvSetAudioSessionData(const nsID& aId,
                                       const nsString& aDisplayName,
                                       const nsString& aIconPath)
 {
 #if defined(XP_WIN)
     if (NS_FAILED(mozilla::widget::RecvAudioSessionData(aId, aDisplayName,
                                                         aIconPath))) {
       return true;
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -614,18 +614,16 @@ public:
 
   virtual PContentPermissionRequestChild*
   AllocPContentPermissionRequestChild(const InfallibleTArray<PermissionRequest>& aRequests,
                                       const IPC::Principal& aPrincipal,
                                       const TabId& aTabId) override;
   virtual bool
   DeallocPContentPermissionRequestChild(PContentPermissionRequestChild* actor) override;
 
-  virtual bool RecvGamepadUpdate(const GamepadChangeEvent& aGamepadEvent) override;
-
   // Windows specific - set up audio session
   virtual bool
   RecvSetAudioSessionData(const nsID& aId,
                           const nsString& aDisplayName,
                           const nsString& aIconPath) override;
 
 private:
   static void ForceKillTimerCallback(nsITimer* aTimer, void* aClosure);
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -256,20 +256,16 @@ using namespace mozilla::system;
 #include "nsIBrowserSearchService.h"
 #endif
 
 #ifdef MOZ_ENABLE_PROFILER_SPS
 #include "nsIProfiler.h"
 #include "nsIProfileSaveEvent.h"
 #endif
 
-#ifdef MOZ_GAMEPAD
-#include "mozilla/dom/GamepadMonitoring.h"
-#endif
-
 #ifndef MOZ_SIMPLEPUSH
 #include "mozilla/dom/PushNotifier.h"
 #endif
 
 #ifdef XP_WIN
 #include "mozilla/widget/AudioSession.h"
 #endif
 
@@ -2303,17 +2299,16 @@ ContentParent::InitializeMembers()
   mMetamorphosed = false;
   mSendPermissionUpdates = false;
   mCalledClose = false;
   mCalledKillHard = false;
   mCreatedPairedMinidumps = false;
   mShutdownPending = false;
   mIPCOpen = true;
   mHangMonitorActor = nullptr;
-  mHasGamepadListener = false;
 }
 
 bool
 ContentParent::LaunchSubprocess(ProcessPriority aInitialPriority /* = PROCESS_PRIORITY_FOREGROUND */)
 {
   PROFILER_LABEL_FUNC(js::ProfileEntry::Category::OTHER);
 
   std::vector<std::string> extraArgs;
@@ -5623,44 +5618,16 @@ ContentParent::GetBrowserConfiguration(c
     swr->GetRegistrations(aConfig.serviceWorkerRegistrations());
     return true;
   }
 
   return ContentChild::GetSingleton()->SendGetBrowserConfiguration(aURI, &aConfig);
 }
 
 bool
-ContentParent::RecvGamepadListenerAdded()
-{
-#ifdef MOZ_GAMEPAD
-  if (mHasGamepadListener) {
-    NS_WARNING("Gamepad listener already started, cannot start again!");
-    return false;
-  }
-  mHasGamepadListener = true;
-  StartGamepadMonitoring();
-#endif
-  return true;
-}
-
-bool
-ContentParent::RecvGamepadListenerRemoved()
-{
-#ifdef MOZ_GAMEPAD
-  if (!mHasGamepadListener) {
-    NS_WARNING("Gamepad listener already stopped, cannot stop again!");
-    return false;
-  }
-  mHasGamepadListener = false;
-  MaybeStopGamepadMonitoring();
-#endif
-  return true;
-}
-
-bool
 ContentParent::RecvProfile(const nsCString& aProfile)
 {
 #ifdef MOZ_ENABLE_PROFILER_SPS
   if (NS_WARN_IF(!mGatherer)) {
     return true;
   }
   mProfile = aProfile;
   mGatherer->GatheredOOPProfile();
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -541,18 +541,16 @@ public:
                                        const IPC::Principal& aPrincipal,
                                        const TabId& aTabId) override;
 
   virtual bool
   DeallocPContentPermissionRequestParent(PContentPermissionRequestParent* actor) override;
 
   virtual bool HandleWindowsMessages(const Message& aMsg) const override;
 
-  bool HasGamepadListener() const { return mHasGamepadListener; }
-
   void SetNuwaParent(NuwaParent* aNuwaParent) { mNuwaParent = aNuwaParent; }
 
   void ForkNewProcess(bool aBlocking);
 
   virtual bool RecvCreateWindow(PBrowserParent* aThisTabParent,
                                 PBrowserParent* aOpener,
                                 layout::PRenderFrameParent* aRenderFrame,
                                 const uint32_t& aChromeFlags,
@@ -1140,20 +1138,16 @@ private:
 
 
   virtual bool RecvUpdateDropEffect(const uint32_t& aDragAction,
                                     const uint32_t& aDropEffect) override;
 
   virtual bool RecvGetBrowserConfiguration(const nsCString& aURI,
                                            BrowserConfiguration* aConfig) override;
 
-  virtual bool RecvGamepadListenerAdded() override;
-
-  virtual bool RecvGamepadListenerRemoved() override;
-
   virtual bool RecvProfile(const nsCString& aProfile) override;
 
   virtual bool RecvGetGraphicsDeviceInitData(DeviceInitData* aOut) override;
 
   void StartProfiler(nsIProfilerStartParams* aParams);
 
   virtual bool RecvGetDeviceStorageLocation(const nsString& aType,
                                             nsString* aPath) override;
@@ -1220,17 +1214,16 @@ private:
 
   // True only the if process is already a browser or app or has
   // been transformed into one.
   bool mMetamorphosed;
 
   bool mSendPermissionUpdates;
   bool mIsForBrowser;
   bool mIsNuwaProcess;
-  bool mHasGamepadListener;
 
   // These variables track whether we've called Close() and KillHard() on our
   // channel.
   bool mCalledClose;
   bool mCalledKillHard;
   bool mCreatedPairedMinidumps;
   bool mShutdownPending;
   bool mIPCOpen;
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -334,47 +334,17 @@ struct DomainPolicyClone
 {
     bool        active;
     URIParams[] blacklist;
     URIParams[] whitelist;
     URIParams[] superBlacklist;
     URIParams[] superWhitelist;
 };
 
-struct GamepadAdded {
-    nsString id;
-    uint32_t index;
-    uint32_t mapping;
-    uint32_t num_buttons;
-    uint32_t num_axes;
-};
 
-struct GamepadRemoved {
-    uint32_t index;
-};
-
-struct GamepadAxisInformation {
-    uint32_t index;
-    uint32_t axis;
-    double value;
-};
-
-struct GamepadButtonInformation {
-    uint32_t index;
-    uint32_t button;
-    bool pressed;
-    double value;
-};
-
-union GamepadChangeEvent {
-    GamepadAdded;
-    GamepadRemoved;
-    GamepadAxisInformation;
-    GamepadButtonInformation;
-};
 
 struct FrameScriptInfo
 {
     nsString url;
     bool runInGlobalScope;
 };
 
 struct AndroidSystemInfo
@@ -632,21 +602,16 @@ child:
 
     /**
      * Requests a full native update of a native plugin child window. This is
      * a Windows specific call.
      */
     async UpdateWindow(uintptr_t aChildId);
 
     /**
-     * Send gamepad status update to child.
-     */
-    async GamepadUpdate(GamepadChangeEvent aGamepadEvent);
-
-    /**
      * Notify the child that presentation receiver has been launched with the
      * correspondent iframe.
      */
     async NotifyPresentationReceiverLaunched(PBrowser aIframe, nsString aSessionId);
 
     /**
      * Notify the child that the info about a presentation receiver needs to be
      * cleaned up.
@@ -1122,26 +1087,16 @@ parent:
                                     TabId tabId);
 
     /**
      * Send ServiceWorkerRegistrationData to child process.
      */
     sync GetBrowserConfiguration(nsCString aUri)
         returns (BrowserConfiguration aConfig);
 
-    /*
-     * Tells the parent to start the gamepad listening service if it hasn't already.
-     */
-    async GamepadListenerAdded();
-
-    /**
-     * Tells the parent to stop the gamepad listening service if it hasn't already.
-     */
-    async GamepadListenerRemoved();
-
     async Profile(nsCString aProfile);
 
     /**
      * Request graphics initialization information from the parent.
      */
     sync GetGraphicsDeviceInitData()
         returns (DeviceInitData aData);
 
--- a/dom/locales/en-US/chrome/dom/dom.properties
+++ b/dom/locales/en-US/chrome/dom/dom.properties
@@ -229,8 +229,10 @@ RewriteYouTubeEmbedPathParams=Rewriting 
 # LOCALIZATION NOTE: Do not translate "ServiceWorker". %1$S is the ServiceWorker scope URL. %2$S is an error string.
 PushMessageDecryptionFailure=The ServiceWorker for scope ‘%1$S’ encountered an error decrypting a push message: ‘%2$S’. For help with encryption, please see https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Using_the_Push_API#Encryption
 # LOCALIZATION NOTE: %1$S is the type of a DOM event. 'passive' is a literal parameter from the DOM spec.
 PreventDefaultFromPassiveListenerWarning=Ignoring ‘preventDefault()’ call on event of type ‘%1$S’ from a listener registered as ‘passive’.
 FileLastModifiedDateWarning=File.lastModifiedDate is deprecated. Use File.lastModified instead.
 ChromeScriptedDOMParserWithoutPrincipal=Creating DOMParser without a principal is deprecated.
 IIRFilterChannelCountChangeWarning=IIRFilterNode channel count changes may produce audio glitches.
 BiquadFilterChannelCountChangeWarning=BiquadFilterNode channel count changes may produce audio glitches.
+# LOCALIZATION NOTE: %1$S is the unanimatable paced property.
+UnanimatablePacedProperty=Paced property ‘%1$S’ is not an animatable property.
--- a/dom/media/Benchmark.cpp
+++ b/dom/media/Benchmark.cpp
@@ -202,18 +202,17 @@ BenchmarkPlayback::DemuxNextSample()
 }
 
 void
 BenchmarkPlayback::InitDecoder(TrackInfo&& aInfo)
 {
   MOZ_ASSERT(OnThread());
 
   RefPtr<PDMFactory> platform = new PDMFactory();
-  mDecoder = platform->CreateDecoder(aInfo, mDecoderTaskQueue, this,
-     /* DecoderDoctorDiagnostics* */ nullptr);
+  mDecoder = platform->CreateDecoder({ aInfo, mDecoderTaskQueue, reinterpret_cast<MediaDataDecoderCallback*>(this) });
   if (!mDecoder) {
     MainThreadShutdown();
     return;
   }
   RefPtr<Benchmark> ref(mMainThreadState);
   mDecoder->Init()->Then(
     Thread(), __func__,
     [this, ref](TrackInfo::TrackType aTrackType) {
--- a/dom/media/MediaFormatReader.cpp
+++ b/dom/media/MediaFormatReader.cpp
@@ -402,38 +402,37 @@ MediaFormatReader::EnsureDecoderCreated(
     }
   }
 
   decoder.mDecoderInitialized = false;
 
   MonitorAutoLock mon(decoder.mMonitor);
 
   switch (aTrack) {
-    case TrackType::kAudioTrack:
-      decoder.mDecoder =
-        mPlatform->CreateDecoder(decoder.mInfo ?
-                                   *decoder.mInfo->GetAsAudioInfo() :
-                                   mInfo.mAudio,
-                                 decoder.mTaskQueue,
-                                 decoder.mCallback,
-                                 /* DecoderDoctorDiagnostics* */ nullptr);
+    case TrackType::kAudioTrack: {
+      decoder.mDecoder = mPlatform->CreateDecoder({
+        decoder.mInfo ? *decoder.mInfo->GetAsAudioInfo() : mInfo.mAudio,
+        decoder.mTaskQueue,
+        decoder.mCallback.get()
+      });
       break;
-    case TrackType::kVideoTrack:
+    }
+
+    case TrackType::kVideoTrack: {
       // Decoders use the layers backend to decide if they can use hardware decoding,
       // so specify LAYERS_NONE if we want to forcibly disable it.
-      decoder.mDecoder =
-        mPlatform->CreateDecoder(mVideo.mInfo ?
-                                   *mVideo.mInfo->GetAsVideoInfo() :
-                                   mInfo.mVideo,
-                                 decoder.mTaskQueue,
-                                 decoder.mCallback,
-                                 /* DecoderDoctorDiagnostics* */ nullptr,
-                                 mLayersBackendType,
-                                 GetImageContainer());
+      decoder.mDecoder = mPlatform->CreateDecoder({
+        mVideo.mInfo ? *mVideo.mInfo->GetAsVideoInfo() : mInfo.mVideo,
+        decoder.mTaskQueue,
+        decoder.mCallback.get(),
+        mLayersBackendType,
+        GetImageContainer(),
+      });
       break;
+    }
     default:
       break;
   }
   if (decoder.mDecoder ) {
     decoder.mDescription = decoder.mDecoder->GetDescriptionName();
   } else {
     decoder.mDescription = "error creating decoder";
   }
--- a/dom/media/MediaInfo.h
+++ b/dom/media/MediaInfo.h
@@ -183,26 +183,31 @@ public:
     kDegree_180 = 180,
     kDegree_270 = 270,
   };
   VideoInfo()
     : VideoInfo(-1, -1)
   {
   }
 
-  VideoInfo(int32_t aWidth, int32_t aHeight)
+  explicit VideoInfo(int32_t aWidth, int32_t aHeight)
+    : VideoInfo(nsIntSize(aWidth, aHeight))
+  {
+  }
+
+  explicit VideoInfo(const nsIntSize& aSize)
     : TrackInfo(kVideoTrack, NS_LITERAL_STRING("2"), NS_LITERAL_STRING("main"),
                 EmptyString(), EmptyString(), true, 2)
-    , mDisplay(nsIntSize(aWidth, aHeight))
+    , mDisplay(aSize)
     , mStereoMode(StereoMode::MONO)
-    , mImage(nsIntSize(aWidth, aHeight))
+    , mImage(aSize)
     , mCodecSpecificConfig(new MediaByteBuffer)
     , mExtraData(new MediaByteBuffer)
     , mRotation(kDegree_0)
-    , mImageRect(nsIntRect(0, 0, aWidth, aHeight))
+    , mImageRect(nsIntRect(nsIntPoint(), aSize))
   {
   }
 
   VideoInfo(const VideoInfo& aOther)
     : TrackInfo(aOther)
     , mDisplay(aOther.mDisplay)
     , mStereoMode(aOther.mStereoMode)
     , mImage(aOther.mImage)
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -1189,30 +1189,30 @@ public:
 
     nsresult rv;
 
     if (mAudioDevice) {
       rv = mAudioDevice->Allocate(GetInvariant(mConstraints.mAudio),
                                   mPrefs, mOrigin);
       if (NS_FAILED(rv)) {
         LOG(("Failed to allocate audiosource %d",rv));
-        Fail(NS_LITERAL_STRING("SourceUnavailableError"),
+        Fail(NS_LITERAL_STRING("NotReadableError"),
              NS_LITERAL_STRING("Failed to allocate audiosource"));
         return NS_OK;
       }
     }
     if (mVideoDevice) {
       rv = mVideoDevice->Allocate(GetInvariant(mConstraints.mVideo),
                                   mPrefs, mOrigin);
       if (NS_FAILED(rv)) {
         LOG(("Failed to allocate videosource %d\n",rv));
         if (mAudioDevice) {
           mAudioDevice->GetSource()->Deallocate();
         }
-        Fail(NS_LITERAL_STRING("SourceUnavailableError"),
+        Fail(NS_LITERAL_STRING("NotReadableError"),
              NS_LITERAL_STRING("Failed to allocate videosource"));
         return NS_OK;
       }
     }
     PeerIdentity* peerIdentity = nullptr;
     if (!mConstraints.mPeerIdentity.IsEmpty()) {
       peerIdentity = new PeerIdentity(mConstraints.mPeerIdentity);
     }
--- a/dom/media/MediaStreamError.cpp
+++ b/dom/media/MediaStreamError.cpp
@@ -20,19 +20,18 @@ BaseMediaMgrError::BaseMediaMgrError(con
   if (mMessage.IsEmpty()) {
     if (mName.EqualsLiteral("NotFoundError")) {
       mMessage.AssignLiteral("The object can not be found here.");
     } else if (mName.EqualsLiteral("NotAllowedError")) {
       mMessage.AssignLiteral("The request is not allowed by the user agent "
                              "or the platform in the current context.");
     } else if (mName.EqualsLiteral("SecurityError")) {
       mMessage.AssignLiteral("The operation is insecure.");
-    } else if (mName.EqualsLiteral("SourceUnavailableError")) {
-      mMessage.AssignLiteral("The source of the MediaStream could not be "
-          "accessed due to a hardware error (e.g. lock from another process).");
+    } else if (mName.EqualsLiteral("NotReadableError")) {
+      mMessage.AssignLiteral("The I/O read operation failed.");
     } else if (mName.EqualsLiteral("InternalError")) {
       mMessage.AssignLiteral("Internal error.");
     } else if (mName.EqualsLiteral("NotSupportedError")) {
       mMessage.AssignLiteral("The operation is not supported.");
     } else if (mName.EqualsLiteral("OverconstrainedError")) {
       mMessage.AssignLiteral("Constraints could be not satisfied.");
     }
   }
--- a/dom/media/VideoFrameContainer.cpp
+++ b/dom/media/VideoFrameContainer.cpp
@@ -131,16 +131,17 @@ void VideoFrameContainer::ClearCurrentFr
   MutexAutoLock lock(mMutex);
 
   // See comment in SetCurrentFrame for the reasoning behind
   // using a kungFuDeathGrip here.
   nsTArray<ImageContainer::OwningImage> kungFuDeathGrip;
   mImageContainer->GetCurrentImages(&kungFuDeathGrip);
 
   mImageContainer->ClearAllImages();
+  mImageContainer->ClearCachedResources();
 }
 
 void VideoFrameContainer::ClearFutureFrames()
 {
   MutexAutoLock lock(mMutex);
 
   // See comment in SetCurrentFrame for the reasoning behind
   // using a kungFuDeathGrip here.
--- a/dom/media/fmp4/MP4Decoder.cpp
+++ b/dom/media/fmp4/MP4Decoder.cpp
@@ -217,20 +217,17 @@ CreateTestH264Decoder(layers::LayersBack
   aConfig.mDuration = 40000;
   aConfig.mMediaTime = 0;
   aConfig.mImage = aConfig.mDisplay = nsIntSize(640, 360);
   aConfig.mExtraData = new MediaByteBuffer();
   aConfig.mExtraData->AppendElements(sTestH264ExtraData,
                                      MOZ_ARRAY_LENGTH(sTestH264ExtraData));
 
   RefPtr<PDMFactory> platform = new PDMFactory();
-  RefPtr<MediaDataDecoder> decoder(
-    platform->CreateDecoder(aConfig, aTaskQueue, nullptr,
-                            /* DecoderDoctorDiagnostics* */ nullptr,
-                            aBackend, nullptr));
+  RefPtr<MediaDataDecoder> decoder(platform->CreateDecoder({ aConfig, aTaskQueue, aBackend }));
 
   return decoder.forget();
 }
 
 /* static */ already_AddRefed<dom::Promise>
 MP4Decoder::IsVideoAccelerated(layers::LayersBackend aBackend, nsIGlobalObject* aParent)
 {
   MOZ_ASSERT(NS_IsMainThread());
--- a/dom/media/platforms/PDMFactory.cpp
+++ b/dom/media/platforms/PDMFactory.cpp
@@ -96,126 +96,94 @@ PDMFactory::EnsureInit() const
       nsCOMPtr<nsIRunnable> runnable =
         NS_NewRunnableFunction([]() { ClearOnShutdown(&sInstance); });
       NS_DispatchToMainThread(runnable);
     }
   }
 }
 
 already_AddRefed<MediaDataDecoder>
-PDMFactory::CreateDecoder(const TrackInfo& aConfig,
-                          TaskQueue* aTaskQueue,
-                          MediaDataDecoderCallback* aCallback,
-                          DecoderDoctorDiagnostics* aDiagnostics,
-                          layers::LayersBackend aLayersBackend,
-                          layers::ImageContainer* aImageContainer) const
+PDMFactory::CreateDecoder(const CreateDecoderParams& aParams)
 {
-  bool isEncrypted = mEMEPDM && aConfig.mCrypto.mValid;
+  const TrackInfo& config = aParams.mConfig;
+  bool isEncrypted = mEMEPDM && config.mCrypto.mValid;
 
   if (isEncrypted) {
-    return CreateDecoderWithPDM(mEMEPDM,
-                                aConfig,
-                                aTaskQueue,
-                                aCallback,
-                                aDiagnostics,
-                                aLayersBackend,
-                                aImageContainer);
+    return CreateDecoderWithPDM(mEMEPDM, aParams);
   }
 
-  if (aDiagnostics) {
+  DecoderDoctorDiagnostics* diagnostics = aParams.mDiagnostics;
+  if (diagnostics) {
     // If libraries failed to load, the following loop over mCurrentPDMs
     // will not even try to use them. So we record failures now.
     if (mWMFFailedToLoad) {
-      aDiagnostics->SetWMFFailedToLoad();
+      diagnostics->SetWMFFailedToLoad();
     }
     if (mFFmpegFailedToLoad) {
-      aDiagnostics->SetFFmpegFailedToLoad();
+      diagnostics->SetFFmpegFailedToLoad();
     }
     if (mGMPPDMFailedToStartup) {
-      aDiagnostics->SetGMPPDMFailedToStartup();
+      diagnostics->SetGMPPDMFailedToStartup();
     }
   }
 
   for (auto& current : mCurrentPDMs) {
-    if (!current->SupportsMimeType(aConfig.mMimeType, aDiagnostics)) {
+    if (!current->SupportsMimeType(config.mMimeType, diagnostics)) {
       continue;
     }
-    RefPtr<MediaDataDecoder> m =
-      CreateDecoderWithPDM(current,
-                           aConfig,
-                           aTaskQueue,
-                           aCallback,
-                           aDiagnostics,
-                           aLayersBackend,
-                           aImageContainer);
+    RefPtr<MediaDataDecoder> m = CreateDecoderWithPDM(current, aParams);
     if (m) {
       return m.forget();
     }
   }
   NS_WARNING("Unable to create a decoder, no platform found.");
   return nullptr;
 }
 
 already_AddRefed<MediaDataDecoder>
 PDMFactory::CreateDecoderWithPDM(PlatformDecoderModule* aPDM,
-                                 const TrackInfo& aConfig,
-                                 TaskQueue* aTaskQueue,
-                                 MediaDataDecoderCallback* aCallback,
-                                 DecoderDoctorDiagnostics* aDiagnostics,
-                                 layers::LayersBackend aLayersBackend,
-                                 layers::ImageContainer* aImageContainer) const
+                                 const CreateDecoderParams& aParams)
 {
   MOZ_ASSERT(aPDM);
   RefPtr<MediaDataDecoder> m;
 
-  if (aConfig.GetAsAudioInfo()) {
-    m = aPDM->CreateAudioDecoder(*aConfig.GetAsAudioInfo(),
-                                 aTaskQueue,
-                                 aCallback,
-                                 aDiagnostics);
+  const TrackInfo& config = aParams.mConfig;
+  if (config.IsAudio()) {
+    m = aPDM->CreateAudioDecoder(aParams);
     return m.forget();
   }
 
-  if (!aConfig.GetAsVideoInfo()) {
+  if (!config.IsVideo()) {
     return nullptr;
   }
 
-  MediaDataDecoderCallback* callback = aCallback;
+  MediaDataDecoderCallback* callback = aParams.mCallback;
   RefPtr<DecoderCallbackFuzzingWrapper> callbackWrapper;
   if (MediaPrefs::PDMFuzzingEnabled()) {
-    callbackWrapper = new DecoderCallbackFuzzingWrapper(aCallback);
+    callbackWrapper = new DecoderCallbackFuzzingWrapper(callback);
     callbackWrapper->SetVideoOutputMinimumInterval(
       TimeDuration::FromMilliseconds(MediaPrefs::PDMFuzzingInterval()));
     callbackWrapper->SetDontDelayInputExhausted(!MediaPrefs::PDMFuzzingDelayInputExhausted());
     callback = callbackWrapper.get();
   }
 
-  if (H264Converter::IsH264(aConfig)) {
-    RefPtr<H264Converter> h
-      = new H264Converter(aPDM,
-                          *aConfig.GetAsVideoInfo(),
-                          aLayersBackend,
-                          aImageContainer,
-                          aTaskQueue,
-                          callback,
-                          aDiagnostics);
+  CreateDecoderParams params = aParams;
+  params.mCallback = callback;
+
+  if (H264Converter::IsH264(config)) {
+    RefPtr<H264Converter> h = new H264Converter(aPDM, params);
     const nsresult rv = h->GetLastError();
     if (NS_SUCCEEDED(rv) || rv == NS_ERROR_NOT_INITIALIZED) {
       // The H264Converter either successfully created the wrapped decoder,
       // or there wasn't enough AVCC data to do so. Otherwise, there was some
       // problem, for example WMF DLLs were missing.
       m = h.forget();
     }
   } else {
-    m = aPDM->CreateVideoDecoder(*aConfig.GetAsVideoInfo(),
-                                 aLayersBackend,
-                                 aImageContainer,
-                                 aTaskQueue,
-                                 callback,
-                                 aDiagnostics);
+    m = aPDM->CreateVideoDecoder(params);
   }
 
   if (callbackWrapper && m) {
     m = new DecoderFuzzingWrapper(m.forget(), callbackWrapper.forget());
   }
 
   return m.forget();
 }
--- a/dom/media/platforms/PDMFactory.h
+++ b/dom/media/platforms/PDMFactory.h
@@ -25,22 +25,17 @@ public:
   PDMFactory();
 
   // Factory method that creates the appropriate PlatformDecoderModule for
   // the platform we're running on. Caller is responsible for deleting this
   // instance. It's expected that there will be multiple
   // PlatformDecoderModules alive at the same time.
   // This is called on the decode task queue.
   already_AddRefed<MediaDataDecoder>
-  CreateDecoder(const TrackInfo& aConfig,
-                TaskQueue* aTaskQueue,
-                MediaDataDecoderCallback* aCallback,
-                DecoderDoctorDiagnostics* aDiagnostics,
-                layers::LayersBackend aLayersBackend = layers::LayersBackend::LAYERS_NONE,
-                layers::ImageContainer* aImageContainer = nullptr) const;
+  CreateDecoder(const CreateDecoderParams& aParams);
 
   bool SupportsMimeType(const nsACString& aMimeType,
                         DecoderDoctorDiagnostics* aDiagnostics) const;
 
 #ifdef MOZ_EME
   // Creates a PlatformDecoderModule that uses a CDMProxy to decrypt or
   // decrypt-and-decode EME encrypted content. If the CDM only decrypts and
   // does not decode, we create a PDM and use that to create MediaDataDecoders
@@ -56,22 +51,17 @@ private:
   bool StartupPDM(PlatformDecoderModule* aPDM);
   // Returns the first PDM in our list supporting the mimetype.
   already_AddRefed<PlatformDecoderModule>
   GetDecoder(const nsACString& aMimeType,
              DecoderDoctorDiagnostics* aDiagnostics) const;
 
   already_AddRefed<MediaDataDecoder>
   CreateDecoderWithPDM(PlatformDecoderModule* aPDM,
-                       const TrackInfo& aConfig,
-                       TaskQueue* aTaskQueue,
-                       MediaDataDecoderCallback* aCallback,
-                       DecoderDoctorDiagnostics* aDiagnostics,
-                       layers::LayersBackend aLayersBackend,
-                       layers::ImageContainer* aImageContainer) const;
+                       const CreateDecoderParams& aParams);
 
   nsTArray<RefPtr<PlatformDecoderModule>> mCurrentPDMs;
   RefPtr<PlatformDecoderModule> mEMEPDM;
 
   bool mWMFFailedToLoad = false;
   bool mFFmpegFailedToLoad = false;
   bool mGMPPDMFailedToStartup = false;
 
--- a/dom/media/platforms/PlatformDecoderModule.h
+++ b/dom/media/platforms/PlatformDecoderModule.h
@@ -27,16 +27,64 @@ class ImageContainer;
 
 class MediaDataDecoder;
 class MediaDataDecoderCallback;
 class TaskQueue;
 class CDMProxy;
 
 static LazyLogModule sPDMLog("PlatformDecoderModule");
 
+struct CreateDecoderParams {
+  explicit CreateDecoderParams(const TrackInfo& aConfig)
+    : mConfig(aConfig)
+  {}
+
+  template <typename T1, typename... Ts>
+  CreateDecoderParams(const TrackInfo& aConfig, T1 a1, Ts... as)
+    : mConfig(aConfig)
+  {
+    Set(a1, as...);
+  }
+
+  const VideoInfo& VideoConfig() const
+  {
+    MOZ_ASSERT(mConfig.IsVideo());
+    return *mConfig.GetAsVideoInfo();
+  }
+
+  const AudioInfo& AudioConfig() const
+  {
+    MOZ_ASSERT(mConfig.IsAudio());
+    return *mConfig.GetAsAudioInfo();
+  }
+
+  const TrackInfo& mConfig;
+  TaskQueue* mTaskQueue = nullptr;
+  MediaDataDecoderCallback* mCallback = nullptr;
+  DecoderDoctorDiagnostics* mDiagnostics = nullptr;
+  layers::ImageContainer* mImageContainer = nullptr;
+  layers::LayersBackend mLayersBackend = layers::LayersBackend::LAYERS_NONE;
+
+private:
+  void Set(TaskQueue* aTaskQueue) { mTaskQueue = aTaskQueue; }
+  void Set(MediaDataDecoderCallback* aCallback) { mCallback = aCallback; }
+  void Set(DecoderDoctorDiagnostics* aDiagnostics) { mDiagnostics = aDiagnostics; }
+  void Set(layers::ImageContainer* aImageContainer) { mImageContainer = aImageContainer; }
+  void Set(layers::LayersBackend aLayersBackend) { mLayersBackend = aLayersBackend; }
+  template <typename T1, typename T2, typename... Ts>
+  void Set(T1 a1, T2 a2, Ts... as)
+  {
+    // Parameter pack expansion trick, to call Set() on each argument.
+    using expander = int[];
+    (void)expander {
+      (Set(a1), 0), (Set(a2), 0), (Set(as), 0)...
+    };
+  }
+};
+
 // The PlatformDecoderModule interface is used by the MediaFormatReader to
 // abstract access to decoders provided by various
 // platforms.
 // Each platform (Windows, MacOSX, Linux, B2G etc) must implement a
 // PlatformDecoderModule to provide access to its decoders in order to get
 // decompressed H.264/AAC from the MediaFormatReader.
 //
 // Decoding is asynchronous, and should be performed on the task queue
@@ -83,38 +131,30 @@ protected:
   // not hold a reference to it.
   // Output and errors should be returned to the reader via aCallback.
   // On Windows the task queue's threads in have MSCOM initialized with
   // COINIT_MULTITHREADED.
   // Returns nullptr if the decoder can't be created.
   // It is safe to store a reference to aConfig.
   // This is called on the decode task queue.
   virtual already_AddRefed<MediaDataDecoder>
-  CreateVideoDecoder(const VideoInfo& aConfig,
-                     layers::LayersBackend aLayersBackend,
-                     layers::ImageContainer* aImageContainer,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) = 0;
+  CreateVideoDecoder(const CreateDecoderParams& aParams) = 0;
 
   // Creates an Audio decoder with the specified properties.
   // Asynchronous decoding of audio should be done in runnables dispatched to
   // aAudioTaskQueue. If the task queue isn't needed, the decoder should
   // not hold a reference to it.
   // Output and errors should be returned to the reader via aCallback.
   // Returns nullptr if the decoder can't be created.
   // On Windows the task queue's threads in have MSCOM initialized with
   // COINIT_MULTITHREADED.
   // It is safe to store a reference to aConfig.
   // This is called on the decode task queue.
   virtual already_AddRefed<MediaDataDecoder>
-  CreateAudioDecoder(const AudioInfo& aConfig,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) = 0;
+  CreateAudioDecoder(const CreateDecoderParams& aParams) = 0;
 };
 
 enum MediaDataDecoderError {
   FATAL_ERROR,
   DECODE_ERROR
 };
 
 // A callback used by MediaDataDecoder to return output/errors to the
--- a/dom/media/platforms/agnostic/AgnosticDecoderModule.cpp
+++ b/dom/media/platforms/agnostic/AgnosticDecoderModule.cpp
@@ -18,51 +18,37 @@ AgnosticDecoderModule::SupportsMimeType(
 {
   return VPXDecoder::IsVPX(aMimeType) ||
     OpusDataDecoder::IsOpus(aMimeType) ||
     VorbisDataDecoder::IsVorbis(aMimeType) ||
     WaveDataDecoder::IsWave(aMimeType);
 }
 
 already_AddRefed<MediaDataDecoder>
-AgnosticDecoderModule::CreateVideoDecoder(const VideoInfo& aConfig,
-                                          layers::LayersBackend aLayersBackend,
-                                          layers::ImageContainer* aImageContainer,
-                                          TaskQueue* aTaskQueue,
-                                          MediaDataDecoderCallback* aCallback,
-                                          DecoderDoctorDiagnostics* aDiagnostics)
+AgnosticDecoderModule::CreateVideoDecoder(const CreateDecoderParams& aParams)
 {
   RefPtr<MediaDataDecoder> m;
 
-  if (VPXDecoder::IsVPX(aConfig.mMimeType)) {
-    m = new VPXDecoder(*aConfig.GetAsVideoInfo(),
-                       aImageContainer,
-                       aTaskQueue,
-                       aCallback);
+  if (VPXDecoder::IsVPX(aParams.mConfig.mMimeType)) {
+    m = new VPXDecoder(aParams);
   }
 
   return m.forget();
 }
 
 already_AddRefed<MediaDataDecoder>
-AgnosticDecoderModule::CreateAudioDecoder(const AudioInfo& aConfig,
-                                          TaskQueue* aTaskQueue,
-                                          MediaDataDecoderCallback* aCallback,
-                                          DecoderDoctorDiagnostics* aDiagnostics)
+AgnosticDecoderModule::CreateAudioDecoder(const CreateDecoderParams& aParams)
 {
   RefPtr<MediaDataDecoder> m;
 
-  if (VorbisDataDecoder::IsVorbis(aConfig.mMimeType)) {
-    m = new VorbisDataDecoder(*aConfig.GetAsAudioInfo(),
-                              aTaskQueue,
-                              aCallback);
-  } else if (OpusDataDecoder::IsOpus(aConfig.mMimeType)) {
-    m = new OpusDataDecoder(*aConfig.GetAsAudioInfo(),
-                            aTaskQueue,
-                            aCallback);
-  } else if (WaveDataDecoder::IsWave(aConfig.mMimeType)) {
-    m = new WaveDataDecoder(*aConfig.GetAsAudioInfo(), aCallback);
+  const TrackInfo& config = aParams.mConfig;
+  if (VorbisDataDecoder::IsVorbis(config.mMimeType)) {
+    m = new VorbisDataDecoder(aParams);
+  } else if (OpusDataDecoder::IsOpus(config.mMimeType)) {
+    m = new OpusDataDecoder(aParams);
+  } else if (WaveDataDecoder::IsWave(config.mMimeType)) {
+    m = new WaveDataDecoder(aParams);
   }
 
   return m.forget();
 }
 
 } // namespace mozilla
--- a/dom/media/platforms/agnostic/AgnosticDecoderModule.h
+++ b/dom/media/platforms/agnostic/AgnosticDecoderModule.h
@@ -17,26 +17,18 @@ public:
   DecoderNeedsConversion(const TrackInfo& aConfig) const override
   {
     return ConversionRequired::kNeedNone;
   }
 
 protected:
   // Decode thread.
   already_AddRefed<MediaDataDecoder>
-  CreateVideoDecoder(const VideoInfo& aConfig,
-                     layers::LayersBackend aLayersBackend,
-                     layers::ImageContainer* aImageContainer,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateVideoDecoder(const CreateDecoderParams& aParams) override;
 
   // Decode thread.
   already_AddRefed<MediaDataDecoder>
-  CreateAudioDecoder(const AudioInfo& aConfig,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateAudioDecoder(const CreateDecoderParams& aParams) override;
 };
 
 } // namespace mozilla
 
 #endif /* AgnosticDecoderModule_h_ */
--- a/dom/media/platforms/agnostic/BlankDecoderModule.cpp
+++ b/dom/media/platforms/agnostic/BlankDecoderModule.cpp
@@ -21,21 +21,20 @@ namespace mozilla {
 
 // Decoder that uses a passed in object's Create function to create blank
 // MediaData objects.
 template<class BlankMediaDataCreator>
 class BlankMediaDataDecoder : public MediaDataDecoder {
 public:
 
   BlankMediaDataDecoder(BlankMediaDataCreator* aCreator,
-                        MediaDataDecoderCallback* aCallback,
-                        TrackInfo::TrackType aType)
+                        const CreateDecoderParams& aParams)
     : mCreator(aCreator)
-    , mCallback(aCallback)
-    , mType(aType)
+    , mCallback(aParams.mCallback)
+    , mType(aParams.mConfig.GetType())
   {
   }
 
   RefPtr<InitPromise> Init() override {
     return InitPromise::CreateAndResolve(mType, __func__);
   }
 
   nsresult Shutdown() override {
@@ -194,44 +193,34 @@ private:
   uint32_t mSampleRate;
 };
 
 class BlankDecoderModule : public PlatformDecoderModule {
 public:
 
   // Decode thread.
   already_AddRefed<MediaDataDecoder>
-  CreateVideoDecoder(const VideoInfo& aConfig,
-                     layers::LayersBackend aLayersBackend,
-                     layers::ImageContainer* aImageContainer,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override {
+  CreateVideoDecoder(const CreateDecoderParams& aParams) override {
+    const VideoInfo& config = aParams.VideoConfig();
     BlankVideoDataCreator* creator = new BlankVideoDataCreator(
-      aConfig.mDisplay.width, aConfig.mDisplay.height, aImageContainer);
+      config.mDisplay.width, config.mDisplay.height, aParams.mImageContainer);
     RefPtr<MediaDataDecoder> decoder =
-      new BlankMediaDataDecoder<BlankVideoDataCreator>(creator,
-                                                       aCallback,
-                                                       TrackInfo::kVideoTrack);
+      new BlankMediaDataDecoder<BlankVideoDataCreator>(creator, aParams);
     return decoder.forget();
   }
 
   // Decode thread.
   already_AddRefed<MediaDataDecoder>
-  CreateAudioDecoder(const AudioInfo& aConfig,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override {
+  CreateAudioDecoder(const CreateDecoderParams& aParams) override {
+    const AudioInfo& config = aParams.AudioConfig();
     BlankAudioDataCreator* creator = new BlankAudioDataCreator(
-      aConfig.mChannels, aConfig.mRate);
+      config.mChannels, config.mRate);
 
     RefPtr<MediaDataDecoder> decoder =
-      new BlankMediaDataDecoder<BlankAudioDataCreator>(creator,
-                                                       aCallback,
-                                                       TrackInfo::kAudioTrack);
+      new BlankMediaDataDecoder<BlankAudioDataCreator>(creator, aParams);
     return decoder.forget();
   }
 
   bool
   SupportsMimeType(const nsACString& aMimeType,
                    DecoderDoctorDiagnostics* aDiagnostics) const override
   {
     return true;
--- a/dom/media/platforms/agnostic/OpusDecoder.cpp
+++ b/dom/media/platforms/agnostic/OpusDecoder.cpp
@@ -15,22 +15,20 @@
 #include <stdint.h>
 #include <inttypes.h>  // For PRId64
 
 #define OPUS_DEBUG(arg, ...) MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, \
     ("OpusDataDecoder(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
 
 namespace mozilla {
 
-OpusDataDecoder::OpusDataDecoder(const AudioInfo& aConfig,
-                                 TaskQueue* aTaskQueue,
-                                 MediaDataDecoderCallback* aCallback)
-  : mInfo(aConfig)
-  , mTaskQueue(aTaskQueue)
-  , mCallback(aCallback)
+OpusDataDecoder::OpusDataDecoder(const CreateDecoderParams& aParams)
+  : mInfo(aParams.AudioConfig())
+  , mTaskQueue(aParams.mTaskQueue)
+  , mCallback(aParams.mCallback)
   , mOpusDecoder(nullptr)
   , mSkip(0)
   , mDecodedHeader(false)
   , mPaddingDiscarded(false)
   , mFrames(0)
   , mIsFlushing(false)
 {
 }
--- a/dom/media/platforms/agnostic/OpusDecoder.h
+++ b/dom/media/platforms/agnostic/OpusDecoder.h
@@ -12,19 +12,17 @@
 #include "mozilla/Maybe.h"
 #include "nsAutoPtr.h"
 
 namespace mozilla {
 
 class OpusDataDecoder : public MediaDataDecoder
 {
 public:
-  OpusDataDecoder(const AudioInfo& aConfig,
-                  TaskQueue* aTaskQueue,
-                  MediaDataDecoderCallback* aCallback);
+  explicit OpusDataDecoder(const CreateDecoderParams& aParams);
   ~OpusDataDecoder();
 
   RefPtr<InitPromise> Init() override;
   nsresult Input(MediaRawData* aSample) override;
   nsresult Flush() override;
   nsresult Drain() override;
   nsresult Shutdown() override;
   const char* GetDescriptionName() const override
--- a/dom/media/platforms/agnostic/VPXDecoder.cpp
+++ b/dom/media/platforms/agnostic/VPXDecoder.cpp
@@ -27,26 +27,23 @@ static int MimeTypeToCodec(const nsACStr
   if (aMimeType.EqualsLiteral("video/webm; codecs=vp8")) {
     return VPXDecoder::Codec::VP8;
   } else if (aMimeType.EqualsLiteral("video/webm; codecs=vp9")) {
     return VPXDecoder::Codec::VP9;
   }
   return -1;
 }
 
-VPXDecoder::VPXDecoder(const VideoInfo& aConfig,
-                       ImageContainer* aImageContainer,
-                       TaskQueue* aTaskQueue,
-                       MediaDataDecoderCallback* aCallback)
-  : mImageContainer(aImageContainer)
-  , mTaskQueue(aTaskQueue)
-  , mCallback(aCallback)
+VPXDecoder::VPXDecoder(const CreateDecoderParams& aParams)
+  : mImageContainer(aParams.mImageContainer)
+  , mTaskQueue(aParams.mTaskQueue)
+  , mCallback(aParams.mCallback)
   , mIsFlushing(false)
-  , mInfo(aConfig)
-  , mCodec(MimeTypeToCodec(aConfig.mMimeType))
+  , mInfo(aParams.VideoConfig())
+  , mCodec(MimeTypeToCodec(aParams.VideoConfig().mMimeType))
 {
   MOZ_COUNT_CTOR(VPXDecoder);
   PodZero(&mVPX);
 }
 
 VPXDecoder::~VPXDecoder()
 {
   MOZ_COUNT_DTOR(VPXDecoder);
--- a/dom/media/platforms/agnostic/VPXDecoder.h
+++ b/dom/media/platforms/agnostic/VPXDecoder.h
@@ -16,21 +16,17 @@
 
 namespace mozilla {
 
 using namespace layers;
 
 class VPXDecoder : public MediaDataDecoder
 {
 public:
-  VPXDecoder(const VideoInfo& aConfig,
-             ImageContainer* aImageContainer,
-             TaskQueue* aTaskQueue,
-             MediaDataDecoderCallback* aCallback);
-
+  explicit VPXDecoder(const CreateDecoderParams& aParams);
   ~VPXDecoder();
 
   RefPtr<InitPromise> Init() override;
   nsresult Input(MediaRawData* aSample) override;
   nsresult Flush() override;
   nsresult Drain() override;
   nsresult Shutdown() override;
   const char* GetDescriptionName() const override
--- a/dom/media/platforms/agnostic/VorbisDecoder.cpp
+++ b/dom/media/platforms/agnostic/VorbisDecoder.cpp
@@ -25,22 +25,20 @@ ogg_packet InitVorbisPacket(const unsign
   packet.bytes = aLength;
   packet.b_o_s = aBOS;
   packet.e_o_s = aEOS;
   packet.granulepos = aGranulepos;
   packet.packetno = aPacketNo;
   return packet;
 }
 
-VorbisDataDecoder::VorbisDataDecoder(const AudioInfo& aConfig,
-                                     TaskQueue* aTaskQueue,
-                                     MediaDataDecoderCallback* aCallback)
-  : mInfo(aConfig)
-  , mTaskQueue(aTaskQueue)
-  , mCallback(aCallback)
+VorbisDataDecoder::VorbisDataDecoder(const CreateDecoderParams& aParams)
+  : mInfo(aParams.AudioConfig())
+  , mTaskQueue(aParams.mTaskQueue)
+  , mCallback(aParams.mCallback)
   , mPacketCount(0)
   , mFrames(0)
   , mIsFlushing(false)
 {
   // Zero these member vars to avoid crashes in Vorbis clear functions when
   // destructor is called before |Init|.
   PodZero(&mVorbisBlock);
   PodZero(&mVorbisDsp);
--- a/dom/media/platforms/agnostic/VorbisDecoder.h
+++ b/dom/media/platforms/agnostic/VorbisDecoder.h
@@ -16,19 +16,17 @@
 #include "vorbis/codec.h"
 #endif
 
 namespace mozilla {
 
 class VorbisDataDecoder : public MediaDataDecoder
 {
 public:
-  VorbisDataDecoder(const AudioInfo& aConfig,
-                TaskQueue* aTaskQueue,
-                MediaDataDecoderCallback* aCallback);
+  explicit VorbisDataDecoder(const CreateDecoderParams& aParams);
   ~VorbisDataDecoder();
 
   RefPtr<InitPromise> Init() override;
   nsresult Input(MediaRawData* aSample) override;
   nsresult Flush() override;
   nsresult Drain() override;
   nsresult Shutdown() override;
   const char* GetDescriptionName() const override
--- a/dom/media/platforms/agnostic/WAVDecoder.cpp
+++ b/dom/media/platforms/agnostic/WAVDecoder.cpp
@@ -40,20 +40,19 @@ DecodeULawSample(uint8_t aValue)
   aValue = aValue ^ 0xFF;
   int8_t sign = (aValue & 0x80) ? -1 : 1;
   uint8_t exponent = (aValue & 0x70) >> 4;
   uint8_t mantissa = aValue & 0x0F;
   int16_t sample = (33 + 2 * mantissa) * (2 << (exponent + 1)) - 33;
   return sign * sample;
 }
 
-WaveDataDecoder::WaveDataDecoder(const AudioInfo& aConfig,
-                                 MediaDataDecoderCallback* aCallback)
-  : mInfo(aConfig)
-  , mCallback(aCallback)
+WaveDataDecoder::WaveDataDecoder(const CreateDecoderParams& aParams)
+  : mInfo(aParams.AudioConfig())
+  , mCallback(aParams.mCallback)
 {
 }
 
 nsresult
 WaveDataDecoder::Shutdown()
 {
   return NS_OK;
 }
--- a/dom/media/platforms/agnostic/WAVDecoder.h
+++ b/dom/media/platforms/agnostic/WAVDecoder.h
@@ -10,18 +10,17 @@
 #include "PlatformDecoderModule.h"
 #include "mp4_demuxer/ByteReader.h"
 
 namespace mozilla {
 
 class WaveDataDecoder : public MediaDataDecoder
 {
 public:
-  WaveDataDecoder(const AudioInfo& aConfig,
-                  MediaDataDecoderCallback* aCallback);
+  explicit WaveDataDecoder(const CreateDecoderParams& aParams);
 
   // Return true if mimetype is Wave
   static bool IsWave(const nsACString& aMimeType);
 
   RefPtr<InitPromise> Init() override;
   nsresult Input(MediaRawData* aSample) override;
   nsresult Flush() override;
   nsresult Drain() override;
--- a/dom/media/platforms/agnostic/eme/EMEAudioDecoder.cpp
+++ b/dom/media/platforms/agnostic/eme/EMEAudioDecoder.cpp
@@ -16,16 +16,23 @@ EMEAudioCallbackAdapter::Error(GMPErr aE
     // The GMP failed to decrypt a frame due to not having a key. This can
     // happen if a key expires or a session is closed during playback.
     NS_WARNING("GMP failed to decrypt due to lack of key");
     return;
   }
   AudioCallbackAdapter::Error(aErr);
 }
 
+EMEAudioDecoder::EMEAudioDecoder(CDMProxy* aProxy,
+                                 const GMPAudioDecoderParams& aParams)
+  : GMPAudioDecoder(GMPAudioDecoderParams(aParams).WithAdapter(
+                    new EMEAudioCallbackAdapter(aParams.mCallback)))
+  , mProxy(aProxy)
+{}
+
 void
 EMEAudioDecoder::InitTags(nsTArray<nsCString>& aTags)
 {
   aTags.AppendElement(NS_LITERAL_CSTRING("aac"));
   aTags.AppendElement(NS_ConvertUTF16toUTF8(mProxy->KeySystem()));
 }
 
 nsCString
--- a/dom/media/platforms/agnostic/eme/EMEAudioDecoder.h
+++ b/dom/media/platforms/agnostic/eme/EMEAudioDecoder.h
@@ -18,24 +18,17 @@ public:
    : AudioCallbackAdapter(aCallback)
   {}
 
   void Error(GMPErr aErr) override;
 };
 
 class EMEAudioDecoder : public GMPAudioDecoder {
 public:
-  EMEAudioDecoder(CDMProxy* aProxy,
-                  const AudioInfo& aConfig,
-                  TaskQueue* aTaskQueue,
-                  MediaDataDecoderCallbackProxy* aCallback)
-   : GMPAudioDecoder(aConfig, aTaskQueue, aCallback, new EMEAudioCallbackAdapter(aCallback))
-   , mProxy(aProxy)
-  {
-  }
+  EMEAudioDecoder(CDMProxy* aProxy, const GMPAudioDecoderParams& aParams);
 
 private:
   void InitTags(nsTArray<nsCString>& aTags) override;
   nsCString GetNodeId() override;
 
   RefPtr<CDMProxy> mProxy;
 };
 
--- a/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp
+++ b/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp
@@ -231,85 +231,66 @@ CreateDecoderWrapper(MediaDataDecoderCal
     return nullptr;
   }
   RefPtr<MediaDataDecoderProxy> decoder(
     new EMEMediaDataDecoderProxy(thread.forget(), aCallback, aProxy, aTaskQueue));
   return decoder.forget();
 }
 
 already_AddRefed<MediaDataDecoder>
-EMEDecoderModule::CreateVideoDecoder(const VideoInfo& aConfig,
-                                     layers::LayersBackend aLayersBackend,
-                                     layers::ImageContainer* aImageContainer,
-                                     TaskQueue* aTaskQueue,
-                                     MediaDataDecoderCallback* aCallback,
-                                     DecoderDoctorDiagnostics* aDiagnostics)
+EMEDecoderModule::CreateVideoDecoder(const CreateDecoderParams& aParams)
 {
-  MOZ_ASSERT(aConfig.mCrypto.mValid);
+  MOZ_ASSERT(aParams.mConfig.mCrypto.mValid);
 
-  if (SupportsMimeType(aConfig.mMimeType, nullptr)) {
+  if (SupportsMimeType(aParams.mConfig.mMimeType, nullptr)) {
     // GMP decodes. Assume that means it can decrypt too.
-    RefPtr<MediaDataDecoderProxy> wrapper = CreateDecoderWrapper(aCallback, mProxy, aTaskQueue);
-    wrapper->SetProxyTarget(new EMEVideoDecoder(mProxy,
-                                                aConfig,
-                                                aLayersBackend,
-                                                aImageContainer,
-                                                aTaskQueue,
-                                                wrapper->Callback()));
+    RefPtr<MediaDataDecoderProxy> wrapper =
+      CreateDecoderWrapper(aParams.mCallback, mProxy, aParams.mTaskQueue);
+    auto params = GMPVideoDecoderParams(aParams).WithCallback(wrapper);
+    wrapper->SetProxyTarget(new EMEVideoDecoder(mProxy, params));
     return wrapper.forget();
   }
 
   MOZ_ASSERT(mPDM);
-  RefPtr<MediaDataDecoder> decoder(
-    mPDM->CreateDecoder(aConfig,
-                        aTaskQueue,
-                        aCallback,
-                        aDiagnostics,
-                        aLayersBackend,
-                        aImageContainer));
+  RefPtr<MediaDataDecoder> decoder(mPDM->CreateDecoder(aParams));
   if (!decoder) {
     return nullptr;
   }
 
   RefPtr<MediaDataDecoder> emeDecoder(new EMEDecryptor(decoder,
-                                                         aCallback,
-                                                         mProxy,
-                                                         AbstractThread::GetCurrent()->AsTaskQueue()));
+                                                       aParams.mCallback,
+                                                       mProxy,
+                                                       AbstractThread::GetCurrent()->AsTaskQueue()));
   return emeDecoder.forget();
 }
 
 already_AddRefed<MediaDataDecoder>
-EMEDecoderModule::CreateAudioDecoder(const AudioInfo& aConfig,
-                                     TaskQueue* aTaskQueue,
-                                     MediaDataDecoderCallback* aCallback,
-                                     DecoderDoctorDiagnostics* aDiagnostics)
+EMEDecoderModule::CreateAudioDecoder(const CreateDecoderParams& aParams)
 {
-  MOZ_ASSERT(aConfig.mCrypto.mValid);
+  MOZ_ASSERT(aParams.mConfig.mCrypto.mValid);
 
-  if (SupportsMimeType(aConfig.mMimeType, nullptr)) {
+  if (SupportsMimeType(aParams.mConfig.mMimeType, nullptr)) {
     // GMP decodes. Assume that means it can decrypt too.
-    RefPtr<MediaDataDecoderProxy> wrapper = CreateDecoderWrapper(aCallback, mProxy, aTaskQueue);
-    wrapper->SetProxyTarget(new EMEAudioDecoder(mProxy,
-                                                aConfig,
-                                                aTaskQueue,
-                                                wrapper->Callback()));
+    RefPtr<MediaDataDecoderProxy> wrapper =
+      CreateDecoderWrapper(aParams.mCallback, mProxy, aParams.mTaskQueue);
+    auto gmpParams = GMPAudioDecoderParams(aParams).WithCallback(wrapper);
+    wrapper->SetProxyTarget(new EMEAudioDecoder(mProxy, gmpParams));
     return wrapper.forget();
   }
 
   MOZ_ASSERT(mPDM);
-  RefPtr<MediaDataDecoder> decoder(
-    mPDM->CreateDecoder(aConfig, aTaskQueue, aCallback, aDiagnostics));
+  RefPtr<MediaDataDecoder> decoder(mPDM->CreateDecoder(aParams));
   if (!decoder) {
     return nullptr;
   }
 
   RefPtr<MediaDataDecoder> emeDecoder(new EMEDecryptor(decoder,
-                                                         aCallback,
-                                                         mProxy,
-                                                         AbstractThread::GetCurrent()->AsTaskQueue()));
+                                                       aParams.mCallback,
+                                                       mProxy,
+                                                       AbstractThread::GetCurrent()->AsTaskQueue()));
   return emeDecoder.forget();
 }
 
 PlatformDecoderModule::ConversionRequired
 EMEDecoderModule::DecoderNeedsConversion(const TrackInfo& aConfig) const
 {
   if (aConfig.IsVideo()) {
     return kNeedAVCC;
--- a/dom/media/platforms/agnostic/eme/EMEDecoderModule.h
+++ b/dom/media/platforms/agnostic/eme/EMEDecoderModule.h
@@ -21,29 +21,21 @@ private:
 public:
   EMEDecoderModule(CDMProxy* aProxy, PDMFactory* aPDM);
 
   virtual ~EMEDecoderModule();
 
 protected:
   // Decode thread.
   already_AddRefed<MediaDataDecoder>
-  CreateVideoDecoder(const VideoInfo& aConfig,
-                    layers::LayersBackend aLayersBackend,
-                    layers::ImageContainer* aImageContainer,
-                    TaskQueue* aTaskQueue,
-                    MediaDataDecoderCallback* aCallback,
-                    DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateVideoDecoder(const CreateDecoderParams& aParams) override;
 
   // Decode thread.
   already_AddRefed<MediaDataDecoder>
-  CreateAudioDecoder(const AudioInfo& aConfig,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateAudioDecoder(const CreateDecoderParams& aParams) override;
 
   ConversionRequired
   DecoderNeedsConversion(const TrackInfo& aConfig) const override;
 
   bool
   SupportsMimeType(const nsACString& aMimeType,
                    DecoderDoctorDiagnostics* aDiagnostics) const override;
 
--- a/dom/media/platforms/agnostic/eme/EMEVideoDecoder.cpp
+++ b/dom/media/platforms/agnostic/eme/EMEVideoDecoder.cpp
@@ -18,16 +18,25 @@ EMEVideoCallbackAdapter::Error(GMPErr aE
     // The GMP failed to decrypt a frame due to not having a key. This can
     // happen if a key expires or a session is closed during playback.
     NS_WARNING("GMP failed to decrypt due to lack of key");
     return;
   }
   VideoCallbackAdapter::Error(aErr);
 }
 
+EMEVideoDecoder::EMEVideoDecoder(CDMProxy* aProxy,
+                                 const GMPVideoDecoderParams& aParams)
+  : GMPVideoDecoder(GMPVideoDecoderParams(aParams).WithAdapter(
+                    new EMEVideoCallbackAdapter(aParams.mCallback,
+                                                VideoInfo(aParams.mConfig.mDisplay),
+                                                aParams.mImageContainer)))
+  , mProxy(aProxy)
+{}
+
 void
 EMEVideoDecoder::InitTags(nsTArray<nsCString>& aTags)
 {
   aTags.AppendElement(NS_LITERAL_CSTRING("h264"));
   aTags.AppendElement(NS_ConvertUTF16toUTF8(mProxy->KeySystem()));
 }
 
 nsCString
--- a/dom/media/platforms/agnostic/eme/EMEVideoDecoder.h
+++ b/dom/media/platforms/agnostic/eme/EMEVideoDecoder.h
@@ -23,34 +23,17 @@ public:
    : VideoCallbackAdapter(aCallback, aVideoInfo, aImageContainer)
   {}
 
   void Error(GMPErr aErr) override;
 };
 
 class EMEVideoDecoder : public GMPVideoDecoder {
 public:
-  EMEVideoDecoder(CDMProxy* aProxy,
-                  const VideoInfo& aConfig,
-                  layers::LayersBackend aLayersBackend,
-                  layers::ImageContainer* aImageContainer,
-                  TaskQueue* aTaskQueue,
-                  MediaDataDecoderCallbackProxy* aCallback)
-   : GMPVideoDecoder(aConfig,
-                     aLayersBackend,
-                     aImageContainer,
-                     aTaskQueue,
-                     aCallback,
-                     new EMEVideoCallbackAdapter(aCallback,
-                                                 VideoInfo(aConfig.mDisplay.width,
-                                                           aConfig.mDisplay.height),
-                                                 aImageContainer))
-   , mProxy(aProxy)
-  {
-  }
+  EMEVideoDecoder(CDMProxy* aProxy, const GMPVideoDecoderParams& aParams);
 
 private:
   void InitTags(nsTArray<nsCString>& aTags) override;
   nsCString GetNodeId() override;
   GMPUniquePtr<GMPVideoEncodedFrame> CreateFrame(MediaRawData* aSample) override;
 
   RefPtr<CDMProxy> mProxy;
 };
--- a/dom/media/platforms/agnostic/gmp/GMPAudioDecoder.cpp
+++ b/dom/media/platforms/agnostic/gmp/GMPAudioDecoder.cpp
@@ -121,16 +121,55 @@ AudioCallbackAdapter::Error(GMPErr aErr)
 
 void
 AudioCallbackAdapter::Terminated()
 {
   NS_WARNING("AAC GMP decoder terminated.");
   mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
 }
 
+GMPAudioDecoderParams::GMPAudioDecoderParams(const CreateDecoderParams& aParams)
+  : mConfig(aParams.AudioConfig())
+  , mTaskQueue(aParams.mTaskQueue)
+  , mCallback(nullptr)
+  , mAdapter(nullptr)
+{}
+
+GMPAudioDecoderParams&
+GMPAudioDecoderParams::WithCallback(MediaDataDecoderProxy* aWrapper)
+{
+  MOZ_ASSERT(aWrapper);
+  MOZ_ASSERT(!mCallback); // Should only be called once per instance.
+  mCallback = aWrapper->Callback();
+  mAdapter = nullptr;
+  return *this;
+}
+
+GMPAudioDecoderParams&
+GMPAudioDecoderParams::WithAdapter(AudioCallbackAdapter* aAdapter)
+{
+  MOZ_ASSERT(aAdapter);
+  MOZ_ASSERT(!mAdapter); // Should only be called once per instance.
+  mCallback = aAdapter->Callback();
+  mAdapter = aAdapter;
+  return *this;
+}
+
+GMPAudioDecoder::GMPAudioDecoder(const GMPAudioDecoderParams& aParams)
+  : mConfig(aParams.mConfig)
+  , mCallback(aParams.mCallback)
+  , mGMP(nullptr)
+  , mAdapter(aParams.mAdapter)
+{
+  MOZ_ASSERT(!mAdapter || mCallback == mAdapter->Callback());
+  if (!mAdapter) {
+    mAdapter = new AudioCallbackAdapter(mCallback);
+  }
+}
+
 void
 GMPAudioDecoder::InitTags(nsTArray<nsCString>& aTags)
 {
   aTags.AppendElement(NS_LITERAL_CSTRING("aac"));
   const Maybe<nsCString> gmp(
     GMPDecoderModule::PreferredGMP(NS_LITERAL_CSTRING("audio/mp4a-latm")));
   if (gmp.isSome()) {
     aTags.AppendElement(gmp.value());
--- a/dom/media/platforms/agnostic/gmp/GMPAudioDecoder.h
+++ b/dom/media/platforms/agnostic/gmp/GMPAudioDecoder.h
@@ -20,16 +20,18 @@ public:
   explicit AudioCallbackAdapter(MediaDataDecoderCallbackProxy* aCallback)
    : mCallback(aCallback)
    , mLastStreamOffset(0)
    , mAudioFrameSum(0)
    , mAudioFrameOffset(0)
    , mMustRecaptureAudioPosition(true)
   {}
 
+  MediaDataDecoderCallbackProxy* Callback() const { return mCallback; }
+
   // GMPAudioDecoderCallbackProxy
   void Decoded(const nsTArray<int16_t>& aPCM, uint64_t aTimeStamp, uint32_t aChannels, uint32_t aRate) override;
   void InputDataExhausted() override;
   void DrainComplete() override;
   void ResetComplete() override;
   void Error(GMPErr aErr) override;
   void Terminated() override;
 
@@ -41,39 +43,30 @@ private:
   MediaDataDecoderCallbackProxy* mCallback;
   int64_t mLastStreamOffset;
 
   int64_t mAudioFrameSum;
   int64_t mAudioFrameOffset;
   bool mMustRecaptureAudioPosition;
 };
 
+struct GMPAudioDecoderParams {
+  explicit GMPAudioDecoderParams(const CreateDecoderParams& aParams);
+  GMPAudioDecoderParams& WithCallback(MediaDataDecoderProxy* aWrapper);
+  GMPAudioDecoderParams& WithAdapter(AudioCallbackAdapter* aAdapter);
+
+  const AudioInfo& mConfig;
+  TaskQueue* mTaskQueue;
+  MediaDataDecoderCallbackProxy* mCallback;
+  AudioCallbackAdapter* mAdapter;
+};
+
 class GMPAudioDecoder : public MediaDataDecoder {
-protected:
-  GMPAudioDecoder(const AudioInfo& aConfig,
-                  TaskQueue* aTaskQueue,
-                  MediaDataDecoderCallbackProxy* aCallback,
-                  AudioCallbackAdapter* aAdapter)
-   : mConfig(aConfig)
-   , mCallback(aCallback)
-   , mGMP(nullptr)
-   , mAdapter(aAdapter)
-  {
-  }
-
 public:
-  GMPAudioDecoder(const AudioInfo& aConfig,
-                  TaskQueue* aTaskQueue,
-                  MediaDataDecoderCallbackProxy* aCallback)
-   : mConfig(aConfig)
-   , mCallback(aCallback)
-   , mGMP(nullptr)
-   , mAdapter(new AudioCallbackAdapter(aCallback))
-  {
-  }
+  explicit GMPAudioDecoder(const GMPAudioDecoderParams& aParams);
 
   RefPtr<InitPromise> Init() override;
   nsresult Input(MediaRawData* aSample) override;
   nsresult Flush() override;
   nsresult Drain() override;
   nsresult Shutdown() override;
   const char* GetDescriptionName() const override
   {
--- a/dom/media/platforms/agnostic/gmp/GMPDecoderModule.cpp
+++ b/dom/media/platforms/agnostic/gmp/GMPDecoderModule.cpp
@@ -41,64 +41,52 @@ CreateDecoderWrapper(MediaDataDecoderCal
   if (!thread) {
     return nullptr;
   }
   RefPtr<MediaDataDecoderProxy> decoder(new MediaDataDecoderProxy(thread.forget(), aCallback));
   return decoder.forget();
 }
 
 already_AddRefed<MediaDataDecoder>
-GMPDecoderModule::CreateVideoDecoder(const VideoInfo& aConfig,
-                                     layers::LayersBackend aLayersBackend,
-                                     layers::ImageContainer* aImageContainer,
-                                     TaskQueue* aTaskQueue,
-                                     MediaDataDecoderCallback* aCallback,
-                                     DecoderDoctorDiagnostics* aDiagnostics)
+GMPDecoderModule::CreateVideoDecoder(const CreateDecoderParams& aParams)
 {
-  if (!aConfig.mMimeType.EqualsLiteral("video/avc")) {
+  if (!aParams.mConfig.mMimeType.EqualsLiteral("video/avc")) {
     return nullptr;
   }
 
-  if (aDiagnostics) {
-    const Maybe<nsCString> preferredGMP = PreferredGMP(aConfig.mMimeType);
+  if (aParams.mDiagnostics) {
+    const Maybe<nsCString> preferredGMP = PreferredGMP(aParams.mConfig.mMimeType);
     if (preferredGMP.isSome()) {
-      aDiagnostics->SetGMP(preferredGMP.value());
+      aParams.mDiagnostics->SetGMP(preferredGMP.value());
     }
   }
 
-  RefPtr<MediaDataDecoderProxy> wrapper = CreateDecoderWrapper(aCallback);
-  wrapper->SetProxyTarget(new GMPVideoDecoder(aConfig,
-                                              aLayersBackend,
-                                              aImageContainer,
-                                              aTaskQueue,
-                                              wrapper->Callback()));
+  RefPtr<MediaDataDecoderProxy> wrapper = CreateDecoderWrapper(aParams.mCallback);
+  auto params = GMPVideoDecoderParams(aParams).WithCallback(wrapper);
+  wrapper->SetProxyTarget(new GMPVideoDecoder(params));
   return wrapper.forget();
 }
 
 already_AddRefed<MediaDataDecoder>
-GMPDecoderModule::CreateAudioDecoder(const AudioInfo& aConfig,
-                                     TaskQueue* aTaskQueue,
-                                     MediaDataDecoderCallback* aCallback,
-                                     DecoderDoctorDiagnostics* aDiagnostics)
+GMPDecoderModule::CreateAudioDecoder(const CreateDecoderParams& aParams)
 {
-  if (!aConfig.mMimeType.EqualsLiteral("audio/mp4a-latm")) {
+  if (!aParams.mConfig.mMimeType.EqualsLiteral("audio/mp4a-latm")) {
     return nullptr;
   }
 
-  if (aDiagnostics) {
-    const Maybe<nsCString> preferredGMP = PreferredGMP(aConfig.mMimeType);
+  if (aParams.mDiagnostics) {
+    const Maybe<nsCString> preferredGMP = PreferredGMP(aParams.mConfig.mMimeType);
     if (preferredGMP.isSome()) {
-      aDiagnostics->SetGMP(preferredGMP.value());
+      aParams.mDiagnostics->SetGMP(preferredGMP.value());
     }
   }
 
-  RefPtr<MediaDataDecoderProxy> wrapper = CreateDecoderWrapper(aCallback);
-  wrapper->SetProxyTarget(new GMPAudioDecoder(aConfig,
-                                              aTaskQueue,
-                                              wrapper->Callback()));
+  RefPtr<MediaDataDecoderProxy> wrapper = CreateDecoderWrapper(aParams.mCallback);
+  auto params = GMPAudioDecoderParams(aParams).WithCallback(wrapper);
+  wrapper->SetProxyTarget(new GMPAudioDecoder(params));
   return wrapper.forget();
 }
 
 PlatformDecoderModule::ConversionRequired
 GMPDecoderModule::DecoderNeedsConversion(const TrackInfo& aConfig) const
 {
   // GMPVideoCodecType::kGMPVideoCodecH264 specifies that encoded frames must be in AVCC format.
   if (aConfig.IsVideo()) {
--- a/dom/media/platforms/agnostic/gmp/GMPDecoderModule.h
+++ b/dom/media/platforms/agnostic/gmp/GMPDecoderModule.h
@@ -28,29 +28,21 @@ namespace mozilla {
 class GMPDecoderModule : public PlatformDecoderModule {
 public:
   GMPDecoderModule();
 
   virtual ~GMPDecoderModule();
 
   // Decode thread.
   already_AddRefed<MediaDataDecoder>
-  CreateVideoDecoder(const VideoInfo& aConfig,
-                     layers::LayersBackend aLayersBackend,
-                     layers::ImageContainer* aImageContainer,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateVideoDecoder(const CreateDecoderParams& aParams) override;
 
   // Decode thread.
   already_AddRefed<MediaDataDecoder>
-  CreateAudioDecoder(const AudioInfo& aConfig,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateAudioDecoder(const CreateDecoderParams& aParams) override;
 
   ConversionRequired
   DecoderNeedsConversion(const TrackInfo& aConfig) const override;
 
   bool
   SupportsMimeType(const nsACString& aMimeType,
                    DecoderDoctorDiagnostics* aDiagnostics) const override;
 
--- a/dom/media/platforms/agnostic/gmp/GMPVideoDecoder.cpp
+++ b/dom/media/platforms/agnostic/gmp/GMPVideoDecoder.cpp
@@ -99,16 +99,62 @@ VideoCallbackAdapter::Error(GMPErr aErr)
 void
 VideoCallbackAdapter::Terminated()
 {
   // Note that this *may* be called from the proxy thread also.
   NS_WARNING("H.264 GMP decoder terminated.");
   mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
 }
 
+GMPVideoDecoderParams::GMPVideoDecoderParams(const CreateDecoderParams& aParams)
+  : mConfig(aParams.VideoConfig())
+  , mTaskQueue(aParams.mTaskQueue)
+  , mCallback(nullptr)
+  , mAdapter(nullptr)
+  , mImageContainer(aParams.mImageContainer)
+  , mLayersBackend(aParams.mLayersBackend)
+{}
+
+GMPVideoDecoderParams&
+GMPVideoDecoderParams::WithCallback(MediaDataDecoderProxy* aWrapper)
+{
+  MOZ_ASSERT(aWrapper);
+  MOZ_ASSERT(!mCallback); // Should only be called once per instance.
+  mCallback = aWrapper->Callback();
+  mAdapter = nullptr;
+  return *this;
+}
+
+GMPVideoDecoderParams&
+GMPVideoDecoderParams::WithAdapter(VideoCallbackAdapter* aAdapter)
+{
+  MOZ_ASSERT(aAdapter);
+  MOZ_ASSERT(!mAdapter); // Should only be called once per instance.
+  mCallback = aAdapter->Callback();
+  mAdapter = aAdapter;
+  return *this;
+}
+
+GMPVideoDecoder::GMPVideoDecoder(const GMPVideoDecoderParams& aParams)
+  : mConfig(aParams.mConfig)
+  , mCallback(aParams.mCallback)
+  , mGMP(nullptr)
+  , mHost(nullptr)
+  , mAdapter(aParams.mAdapter)
+  , mConvertNALUnitLengths(false)
+{
+  MOZ_ASSERT(!mAdapter || mCallback == mAdapter->Callback());
+  if (!mAdapter) {
+    mAdapter = new VideoCallbackAdapter(mCallback,
+                                        VideoInfo(mConfig.mDisplay.width,
+                                                  mConfig.mDisplay.height),
+                                        aParams.mImageContainer);
+  }
+}
+
 void
 GMPVideoDecoder::InitTags(nsTArray<nsCString>& aTags)
 {
   aTags.AppendElement(NS_LITERAL_CSTRING("h264"));
   const Maybe<nsCString> gmp(
     GMPDecoderModule::PreferredGMP(NS_LITERAL_CSTRING("video/avc")));
   if (gmp.isSome()) {
     aTags.AppendElement(gmp.value());
--- a/dom/media/platforms/agnostic/gmp/GMPVideoDecoder.h
+++ b/dom/media/platforms/agnostic/gmp/GMPVideoDecoder.h
@@ -22,16 +22,18 @@ public:
                        VideoInfo aVideoInfo,
                        layers::ImageContainer* aImageContainer)
    : mCallback(aCallback)
    , mLastStreamOffset(0)
    , mVideoInfo(aVideoInfo)
    , mImageContainer(aImageContainer)
   {}
 
+  MediaDataDecoderCallbackProxy* Callback() const { return mCallback; }
+
   // GMPVideoDecoderCallbackProxy
   void Decoded(GMPVideoi420Frame* aDecodedFrame) override;
   void ReceivedDecodedReferenceFrame(const uint64_t aPictureId) override;
   void ReceivedDecodedFrame(const uint64_t aPictureId) override;
   void InputDataExhausted() override;
   void DrainComplete() override;
   void ResetComplete() override;
   void Error(GMPErr aErr) override;
@@ -44,50 +46,32 @@ public:
 private:
   MediaDataDecoderCallbackProxy* mCallback;
   int64_t mLastStreamOffset;
 
   VideoInfo mVideoInfo;
   RefPtr<layers::ImageContainer> mImageContainer;
 };
 
-class GMPVideoDecoder : public MediaDataDecoder {
-protected:
-  GMPVideoDecoder(const VideoInfo& aConfig,
-                  layers::LayersBackend aLayersBackend,
-                  layers::ImageContainer* aImageContainer,
-                  TaskQueue* aTaskQueue,
-                  MediaDataDecoderCallbackProxy* aCallback,
-                  VideoCallbackAdapter* aAdapter)
-   : mConfig(aConfig)
-   , mCallback(aCallback)
-   , mGMP(nullptr)
-   , mHost(nullptr)
-   , mAdapter(aAdapter)
-   , mConvertNALUnitLengths(false)
-  {
-  }
+struct GMPVideoDecoderParams {
+  explicit GMPVideoDecoderParams(const CreateDecoderParams& aParams);
+  GMPVideoDecoderParams& WithCallback(MediaDataDecoderProxy* aWrapper);
+  GMPVideoDecoderParams& WithAdapter(VideoCallbackAdapter* aAdapter);
 
+  const VideoInfo& mConfig;
+  TaskQueue* mTaskQueue;
+  MediaDataDecoderCallbackProxy* mCallback;
+  VideoCallbackAdapter* mAdapter;
+  layers::ImageContainer* mImageContainer;
+  layers::LayersBackend mLayersBackend;
+};
+
+class GMPVideoDecoder : public MediaDataDecoder {
 public:
-  GMPVideoDecoder(const VideoInfo& aConfig,
-                  layers::LayersBackend aLayersBackend,
-                  layers::ImageContainer* aImageContainer,
-                  TaskQueue* aTaskQueue,
-                  MediaDataDecoderCallbackProxy* aCallback)
-   : mConfig(aConfig)
-   , mCallback(aCallback)
-   , mGMP(nullptr)
-   , mHost(nullptr)
-   , mAdapter(new VideoCallbackAdapter(aCallback,
-                                       VideoInfo(aConfig.mDisplay.width,
-                                                 aConfig.mDisplay.height),
-                                       aImageContainer))
-   , mConvertNALUnitLengths(false)
-  {
-  }
+  explicit GMPVideoDecoder(const GMPVideoDecoderParams& aParams);
 
   RefPtr<InitPromise> Init() override;
   nsresult Input(MediaRawData* aSample) override;
   nsresult Flush() override;
   nsresult Drain() override;
   nsresult Shutdown() override;
   const char* GetDescriptionName() const override
   {
--- a/dom/media/platforms/android/AndroidDecoderModule.cpp
+++ b/dom/media/platforms/android/AndroidDecoderModule.cpp
@@ -277,57 +277,55 @@ AndroidDecoderModule::SupportsMimeType(c
     return false;
   }
 
   return widget::HardwareCodecCapabilityUtils::FindDecoderCodecInfoForMimeType(
       nsCString(TranslateMimeType(aMimeType)));
 }
 
 already_AddRefed<MediaDataDecoder>
-AndroidDecoderModule::CreateVideoDecoder(
-    const VideoInfo& aConfig, layers::LayersBackend aLayersBackend,
-    layers::ImageContainer* aImageContainer, TaskQueue* aTaskQueue,
-    MediaDataDecoderCallback* aCallback,
-    DecoderDoctorDiagnostics* aDiagnostics)
+AndroidDecoderModule::CreateVideoDecoder(const CreateDecoderParams& aParams)
 {
   MediaFormat::LocalRef format;
 
+  const VideoInfo& config = aParams.VideoConfig();
   NS_ENSURE_SUCCESS(MediaFormat::CreateVideoFormat(
-      TranslateMimeType(aConfig.mMimeType),
-      aConfig.mDisplay.width,
-      aConfig.mDisplay.height,
+      TranslateMimeType(config.mMimeType),
+      config.mDisplay.width,
+      config.mDisplay.height,
       &format), nullptr);
 
   RefPtr<MediaDataDecoder> decoder =
-    new VideoDataDecoder(aConfig, format, aCallback, aImageContainer);
+    new VideoDataDecoder(config,
+                         format,
+                         aParams.mCallback,
+                         aParams.mImageContainer);
 
   return decoder.forget();
 }
 
 already_AddRefed<MediaDataDecoder>
-AndroidDecoderModule::CreateAudioDecoder(
-    const AudioInfo& aConfig, TaskQueue* aTaskQueue,
-    MediaDataDecoderCallback* aCallback,
-    DecoderDoctorDiagnostics* aDiagnostics)
+AndroidDecoderModule::CreateAudioDecoder(const CreateDecoderParams& aParams)
 {
-  MOZ_ASSERT(aConfig.mBitDepth == 16, "We only handle 16-bit audio!");
+  const AudioInfo& config = aParams.AudioConfig();
+  MOZ_ASSERT(config.mBitDepth == 16, "We only handle 16-bit audio!");
 
   MediaFormat::LocalRef format;
 
   LOG("CreateAudioFormat with mimeType=%s, mRate=%d, channels=%d",
-      aConfig.mMimeType.Data(), aConfig.mRate, aConfig.mChannels);
+      config.mMimeType.Data(), config.mRate, config.mChannels);
 
   NS_ENSURE_SUCCESS(MediaFormat::CreateAudioFormat(
-      aConfig.mMimeType,
-      aConfig.mRate,
-      aConfig.mChannels,
+      config.mMimeType,
+      config.mRate,
+      config.mChannels,
       &format), nullptr);
 
   RefPtr<MediaDataDecoder> decoder =
-    new AudioDataDecoder(aConfig, format, aCallback);
+    new AudioDataDecoder(config, format, aParams.mCallback);
 
   return decoder.forget();
 }
 
 PlatformDecoderModule::ConversionRequired
 AndroidDecoderModule::DecoderNeedsConversion(const TrackInfo& aConfig) const
 {
   if (aConfig.IsVideo()) {
--- a/dom/media/platforms/android/AndroidDecoderModule.h
+++ b/dom/media/platforms/android/AndroidDecoderModule.h
@@ -17,28 +17,20 @@
 
 namespace mozilla {
 
 typedef std::deque<RefPtr<MediaRawData>> SampleQueue;
 
 class AndroidDecoderModule : public PlatformDecoderModule {
 public:
   already_AddRefed<MediaDataDecoder>
-  CreateVideoDecoder(const VideoInfo& aConfig,
-                     layers::LayersBackend aLayersBackend,
-                     layers::ImageContainer* aImageContainer,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateVideoDecoder(const CreateDecoderParams& aParams) override;
 
   already_AddRefed<MediaDataDecoder>
-  CreateAudioDecoder(const AudioInfo& aConfig,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateAudioDecoder(const CreateDecoderParams& aParams) override;
 
 
   AndroidDecoderModule() {}
   virtual ~AndroidDecoderModule() {}
 
   bool SupportsMimeType(const nsACString& aMimeType,
                         DecoderDoctorDiagnostics* aDiagnostics) const override;
 
--- a/dom/media/platforms/apple/AppleDecoderModule.cpp
+++ b/dom/media/platforms/apple/AppleDecoderModule.cpp
@@ -69,52 +69,49 @@ AppleDecoderModule::Startup()
 {
   if (!sInitialized || (!sIsVDAAvailable && !sIsVTAvailable)) {
     return NS_ERROR_FAILURE;
   }
   return NS_OK;
 }
 
 already_AddRefed<MediaDataDecoder>
-AppleDecoderModule::CreateVideoDecoder(const VideoInfo& aConfig,
-                                       layers::LayersBackend aLayersBackend,
-                                       layers::ImageContainer* aImageContainer,
-                                       TaskQueue* aTaskQueue,
-                                       MediaDataDecoderCallback* aCallback,
-                                       DecoderDoctorDiagnostics* aDiagnostics)
+AppleDecoderModule::CreateVideoDecoder(const CreateDecoderParams& aParams)
 {
   RefPtr<MediaDataDecoder> decoder;
 
   if (sIsVDAAvailable && (!sIsVTHWAvailable || MediaPrefs::AppleForceVDA())) {
     decoder =
-      AppleVDADecoder::CreateVDADecoder(aConfig,
-                                        aTaskQueue,
-                                        aCallback,
-                                        aImageContainer);
+      AppleVDADecoder::CreateVDADecoder(aParams.VideoConfig(),
+                                        aParams.mTaskQueue,
+                                        aParams.mCallback,
+                                        aParams.mImageContainer);
     if (decoder) {
       return decoder.forget();
     }
   }
   // We fallback here if VDA isn't available, or is available but isn't
   // supported by the current platform.
   if (sIsVTAvailable) {
     decoder =
-      new AppleVTDecoder(aConfig, aTaskQueue, aCallback, aImageContainer);
+      new AppleVTDecoder(aParams.VideoConfig(),
+                         aParams.mTaskQueue,
+                         aParams.mCallback,
+                         aParams.mImageContainer);
   }
   return decoder.forget();
 }
 
 already_AddRefed<MediaDataDecoder>
-AppleDecoderModule::CreateAudioDecoder(const AudioInfo& aConfig,
-                                       TaskQueue* aTaskQueue,
-                                       MediaDataDecoderCallback* aCallback,
-                                       DecoderDoctorDiagnostics* aDiagnostics)
+AppleDecoderModule::CreateAudioDecoder(const CreateDecoderParams& aParams)
 {
   RefPtr<MediaDataDecoder> decoder =
-    new AppleATDecoder(aConfig, aTaskQueue, aCallback);
+    new AppleATDecoder(aParams.AudioConfig(),
+                       aParams.mTaskQueue,
+                       aParams.mCallback);
   return decoder.forget();
 }
 
 bool
 AppleDecoderModule::SupportsMimeType(const nsACString& aMimeType,
                                      DecoderDoctorDiagnostics* aDiagnostics) const
 {
   return (sIsCoreMediaAvailable &&
--- a/dom/media/platforms/apple/AppleDecoderModule.h
+++ b/dom/media/platforms/apple/AppleDecoderModule.h
@@ -15,29 +15,21 @@ class AppleDecoderModule : public Platfo
 public:
   AppleDecoderModule();
   virtual ~AppleDecoderModule();
 
   nsresult Startup() override;
 
   // Decode thread.
   already_AddRefed<MediaDataDecoder>
-  CreateVideoDecoder(const VideoInfo& aConfig,
-                     layers::LayersBackend aLayersBackend,
-                     layers::ImageContainer* aImageContainer,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateVideoDecoder(const CreateDecoderParams& aParams) override;
 
   // Decode thread.
   already_AddRefed<MediaDataDecoder>
-  CreateAudioDecoder(const AudioInfo& aConfig,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateAudioDecoder(const CreateDecoderParams& aParams) override;
 
   bool SupportsMimeType(const nsACString& aMimeType,
                         DecoderDoctorDiagnostics* aDiagnostics) const override;
 
   ConversionRequired
   DecoderNeedsConversion(const TrackInfo& aConfig) const override;
 
   static void Init();
--- a/dom/media/platforms/ffmpeg/FFmpegDecoderModule.h
+++ b/dom/media/platforms/ffmpeg/FFmpegDecoderModule.h
@@ -26,40 +26,38 @@ public:
 
     return pdm.forget();
   }
 
   explicit FFmpegDecoderModule(FFmpegLibWrapper* aLib) : mLib(aLib) {}
   virtual ~FFmpegDecoderModule() {}
 
   already_AddRefed<MediaDataDecoder>
-  CreateVideoDecoder(const VideoInfo& aConfig,
-                     layers::LayersBackend aLayersBackend,
-                     layers::ImageContainer* aImageContainer,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override
+  CreateVideoDecoder(const CreateDecoderParams& aParams) override
   {
     RefPtr<MediaDataDecoder> decoder =
-      new FFmpegVideoDecoder<V>(mLib, aTaskQueue, aCallback, aConfig,
-                                aImageContainer);
+      new FFmpegVideoDecoder<V>(mLib,
+                                aParams.mTaskQueue,
+                                aParams.mCallback,
+                                aParams.VideoConfig(),
+                                aParams.mImageContainer);
     return decoder.forget();
   }
 
   already_AddRefed<MediaDataDecoder>
-  CreateAudioDecoder(const AudioInfo& aConfig,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override
+  CreateAudioDecoder(const CreateDecoderParams& aParams) override
   {
 #ifdef USING_MOZFFVPX
     return nullptr;
 #else
     RefPtr<MediaDataDecoder> decoder =
-      new FFmpegAudioDecoder<V>(mLib, aTaskQueue, aCallback, aConfig);
+      new FFmpegAudioDecoder<V>(mLib,
+                                aParams.mTaskQueue,
+                                aParams.mCallback,
+                                aParams.AudioConfig());
     return decoder.forget();
 #endif
   }
 
   bool SupportsMimeType(const nsACString& aMimeType,
                         DecoderDoctorDiagnostics* aDiagnostics) const override
   {
     AVCodecID videoCodec = FFmpegVideoDecoder<V>::GetCodecId(aMimeType);
--- a/dom/media/platforms/gonk/GonkDecoderModule.cpp
+++ b/dom/media/platforms/gonk/GonkDecoderModule.cpp
@@ -14,34 +14,26 @@ GonkDecoderModule::GonkDecoderModule()
 {
 }
 
 GonkDecoderModule::~GonkDecoderModule()
 {
 }
 
 already_AddRefed<MediaDataDecoder>
-GonkDecoderModule::CreateVideoDecoder(const VideoInfo& aConfig,
-                                     mozilla::layers::LayersBackend aLayersBackend,
-                                     mozilla::layers::ImageContainer* aImageContainer,
-                                     TaskQueue* aTaskQueue,
-                                     MediaDataDecoderCallback* aCallback,
-                                     DecoderDoctorDiagnostics* aDiagnostics)
+GonkDecoderModule::CreateVideoDecoder(const CreateDecoderParams& aParams)
 {
   RefPtr<MediaDataDecoder> decoder =
   new GonkMediaDataDecoder(new GonkVideoDecoderManager(aImageContainer, aConfig),
                            aCallback);
   return decoder.forget();
 }
 
 already_AddRefed<MediaDataDecoder>
-GonkDecoderModule::CreateAudioDecoder(const AudioInfo& aConfig,
-                                      TaskQueue* aTaskQueue,
-                                      MediaDataDecoderCallback* aCallback,
-                                      DecoderDoctorDiagnostics* aDiagnostics)
+GonkDecoderModule::CreateAudioDecoder(const CreateDecoderParams& aParams)
 {
   RefPtr<MediaDataDecoder> decoder =
   new GonkMediaDataDecoder(new GonkAudioDecoderManager(aConfig),
                            aCallback);
   return decoder.forget();
 }
 
 PlatformDecoderModule::ConversionRequired
--- a/dom/media/platforms/gonk/GonkDecoderModule.h
+++ b/dom/media/platforms/gonk/GonkDecoderModule.h
@@ -13,29 +13,21 @@ namespace mozilla {
 
 class GonkDecoderModule : public PlatformDecoderModule {
 public:
   GonkDecoderModule();
   virtual ~GonkDecoderModule();
 
   // Decode thread.
   already_AddRefed<MediaDataDecoder>
-  CreateVideoDecoder(const VideoInfo& aConfig,
-                     mozilla::layers::LayersBackend aLayersBackend,
-                     mozilla::layers::ImageContainer* aImageContainer,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateVideoDecoder(const CreateDecoderParams& aParams) override;
 
   // Decode thread.
   already_AddRefed<MediaDataDecoder>
-  CreateAudioDecoder(const AudioInfo& aConfig,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateAudioDecoder(const CreateDecoderParams& aParams) override;
 
   ConversionRequired
   DecoderNeedsConversion(const TrackInfo& aConfig) const override;
 
   bool SupportsMimeType(const nsACString& aMimeType,
                         DecoderDoctorDiagnostics* aDiagnostics) const override;
 
 };
--- a/dom/media/platforms/omx/OmxDecoderModule.cpp
+++ b/dom/media/platforms/omx/OmxDecoderModule.cpp
@@ -7,34 +7,30 @@
 #include "OmxDecoderModule.h"
 
 #include "OmxDataDecoder.h"
 #include "OmxPlatformLayer.h"
 
 namespace mozilla {
 
 already_AddRefed<MediaDataDecoder>
-OmxDecoderModule::CreateVideoDecoder(const VideoInfo& aConfig,
-                                     mozilla::layers::LayersBackend aLayersBackend,
-                                     mozilla::layers::ImageContainer* aImageContainer,
-                                     TaskQueue* aTaskQueue,
-                                     MediaDataDecoderCallback* aCallback,
-                                     DecoderDoctorDiagnostics* aDiagnostics)
+OmxDecoderModule::CreateVideoDecoder(const CreateDecoderParams& aParams)
 {
-  RefPtr<OmxDataDecoder> decoder = new OmxDataDecoder(aConfig, aCallback, aImageContainer);
+  RefPtr<OmxDataDecoder> decoder = new OmxDataDecoder(aParams.mConfig,
+                                                      aParams.mCallback,
+                                                      aParams.mImageContainer);
   return decoder.forget();
 }
 
 already_AddRefed<MediaDataDecoder>
-OmxDecoderModule::CreateAudioDecoder(const AudioInfo& aConfig,
-                                     TaskQueue* aTaskQueue,
-                                     MediaDataDecoderCallback* aCallback,
-                                     DecoderDoctorDiagnostics* aDiagnostics)
+OmxDecoderModule::CreateAudioDecoder(const CreateDecoderParams& aParams)
 {
-  RefPtr<OmxDataDecoder> decoder = new OmxDataDecoder(aConfig, aCallback, nullptr);
+  RefPtr<OmxDataDecoder> decoder = new OmxDataDecoder(aParams.mConfig,
+                                                      aParams.mCallback,
+                                                      nullptr);
   return decoder.forget();
 }
 
 PlatformDecoderModule::ConversionRequired
 OmxDecoderModule::DecoderNeedsConversion(const TrackInfo& aConfig) const
 {
   return kNeedNone;
 }
--- a/dom/media/platforms/omx/OmxDecoderModule.h
+++ b/dom/media/platforms/omx/OmxDecoderModule.h
@@ -9,28 +9,20 @@
 
 #include "PlatformDecoderModule.h"
 
 namespace mozilla {
 
 class OmxDecoderModule : public PlatformDecoderModule {
 public:
   already_AddRefed<MediaDataDecoder>
-  CreateVideoDecoder(const VideoInfo& aConfig,
-                     mozilla::layers::LayersBackend aLayersBackend,
-                     mozilla::layers::ImageContainer* aImageContainer,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateVideoDecoder(const CreateDecoderParams& aParams) override;
 
   already_AddRefed<MediaDataDecoder>
-  CreateAudioDecoder(const AudioInfo& aConfig,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateAudioDecoder(const CreateDecoderParams& aParams) override;
 
   bool SupportsMimeType(const nsACString& aMimeType,
                         DecoderDoctorDiagnostics* aDiagnostics) const override;
 
   ConversionRequired DecoderNeedsConversion(const TrackInfo& aConfig) const override;
 };
 
 } // namespace mozilla
--- a/dom/media/platforms/wmf/WMFDecoderModule.cpp
+++ b/dom/media/platforms/wmf/WMFDecoderModule.cpp
@@ -71,53 +71,45 @@ WMFDecoderModule::GetNumDecoderThreads()
 nsresult
 WMFDecoderModule::Startup()
 {
   mWMFInitialized = SUCCEEDED(wmf::MFStartup());
   return mWMFInitialized ? NS_OK : NS_ERROR_FAILURE;
 }
 
 already_AddRefed<MediaDataDecoder>
-WMFDecoderModule::CreateVideoDecoder(const VideoInfo& aConfig,
-                                     layers::LayersBackend aLayersBackend,
-                                     layers::ImageContainer* aImageContainer,
-                                     TaskQueue* aTaskQueue,
-                                     MediaDataDecoderCallback* aCallback,
-                                     DecoderDoctorDiagnostics* aDiagnostics)
+WMFDecoderModule::CreateVideoDecoder(const CreateDecoderParams& aParams)
 {
   nsAutoPtr<WMFVideoMFTManager> manager(
-    new WMFVideoMFTManager(aConfig,
-                           aLayersBackend,
-                           aImageContainer,
+    new WMFVideoMFTManager(aParams.VideoConfig(),
+                           aParams.mLayersBackend,
+                           aParams.mImageContainer,
                            sDXVAEnabled));
 
   if (!manager->Init()) {
     return nullptr;
   }
 
   RefPtr<MediaDataDecoder> decoder =
-    new WMFMediaDataDecoder(manager.forget(), aTaskQueue, aCallback);
+    new WMFMediaDataDecoder(manager.forget(), aParams.mTaskQueue, aParams.mCallback);
 
   return decoder.forget();
 }
 
 already_AddRefed<MediaDataDecoder>
-WMFDecoderModule::CreateAudioDecoder(const AudioInfo& aConfig,
-                                     TaskQueue* aTaskQueue,
-                                     MediaDataDecoderCallback* aCallback,
-                                     DecoderDoctorDiagnostics* aDiagnostics)
+WMFDecoderModule::CreateAudioDecoder(const CreateDecoderParams& aParams)
 {
-  nsAutoPtr<WMFAudioMFTManager> manager(new WMFAudioMFTManager(aConfig));
+  nsAutoPtr<WMFAudioMFTManager> manager(new WMFAudioMFTManager(aParams.AudioConfig()));
 
   if (!manager->Init()) {
     return nullptr;
   }
 
   RefPtr<MediaDataDecoder> decoder =
-    new WMFMediaDataDecoder(manager.forget(), aTaskQueue, aCallback);
+    new WMFMediaDataDecoder(manager.forget(), aParams.mTaskQueue, aParams.mCallback);
   return decoder.forget();
 }
 
 static bool
 CanCreateMFTDecoder(const GUID& aGuid)
 {
   if (FAILED(wmf::MFStartup())) {
     return false;
--- a/dom/media/platforms/wmf/WMFDecoderModule.h
+++ b/dom/media/platforms/wmf/WMFDecoderModule.h
@@ -15,28 +15,20 @@ class WMFDecoderModule : public Platform
 public:
   WMFDecoderModule();
   virtual ~WMFDecoderModule();
 
   // Initializes the module, loads required dynamic libraries, etc.
   nsresult Startup() override;
 
   already_AddRefed<MediaDataDecoder>
-  CreateVideoDecoder(const VideoInfo& aConfig,
-                     layers::LayersBackend aLayersBackend,
-                     layers::ImageContainer* aImageContainer,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateVideoDecoder(const CreateDecoderParams& aParams) override;
 
   already_AddRefed<MediaDataDecoder>
-  CreateAudioDecoder(const AudioInfo& aConfig,
-                     TaskQueue* aTaskQueue,
-                     MediaDataDecoderCallback* aCallback,
-                     DecoderDoctorDiagnostics* aDiagnostics) override;
+  CreateAudioDecoder(const CreateDecoderParams& aParams) override;
 
   bool SupportsMimeType(const nsACString& aMimeType,
                         DecoderDoctorDiagnostics* aDiagnostics) const override;
 
   ConversionRequired
   DecoderNeedsConversion(const TrackInfo& aConfig) const override;
 
   // Called on main thread.
--- a/dom/media/platforms/wrappers/H264Converter.cpp
+++ b/dom/media/platforms/wrappers/H264Converter.cpp
@@ -11,34 +11,29 @@
 #include "MediaInfo.h"
 #include "mp4_demuxer/AnnexB.h"
 #include "mp4_demuxer/H264.h"
 
 namespace mozilla
 {
 
 H264Converter::H264Converter(PlatformDecoderModule* aPDM,
-                             const VideoInfo& aConfig,
-                             layers::LayersBackend aLayersBackend,
-                             layers::ImageContainer* aImageContainer,
-                             TaskQueue* aTaskQueue,
-                             MediaDataDecoderCallback* aCallback,
-                             DecoderDoctorDiagnostics* aDiagnostics)
+                             const CreateDecoderParams& aParams)
   : mPDM(aPDM)
-  , mOriginalConfig(aConfig)
-  , mCurrentConfig(aConfig)
-  , mLayersBackend(aLayersBackend)
-  , mImageContainer(aImageContainer)
-  , mTaskQueue(aTaskQueue)
-  , mCallback(aCallback)
+  , mOriginalConfig(aParams.VideoConfig())
+  , mCurrentConfig(aParams.VideoConfig())
+  , mLayersBackend(aParams.mLayersBackend)
+  , mImageContainer(aParams.mImageContainer)
+  , mTaskQueue(aParams.mTaskQueue)
+  , mCallback(aParams.mCallback)
   , mDecoder(nullptr)
-  , mNeedAVCC(aPDM->DecoderNeedsConversion(aConfig) == PlatformDecoderModule::kNeedAVCC)
+  , mNeedAVCC(aPDM->DecoderNeedsConversion(aParams.mConfig) == PlatformDecoderModule::kNeedAVCC)
   , mLastError(NS_OK)
 {
-  CreateDecoder(aDiagnostics);
+  CreateDecoder(aParams.mDiagnostics);
 }
 
 H264Converter::~H264Converter()
 {
 }
 
 RefPtr<MediaDataDecoder::InitPromise>
 H264Converter::Init()
@@ -141,22 +136,25 @@ H264Converter::CreateDecoder(DecoderDoct
   }
   UpdateConfigFromExtraData(mCurrentConfig.mExtraData);
   if (!mNeedAVCC) {
     // When using a decoder handling AnnexB, we get here only once from the
     // constructor. We do want to get the dimensions extracted from the SPS.
     mOriginalConfig = mCurrentConfig;
   }
 
-  mDecoder = mPDM->CreateVideoDecoder(mNeedAVCC ? mCurrentConfig : mOriginalConfig,
-                                      mLayersBackend,
-                                      mImageContainer,
-                                      mTaskQueue,
-                                      mCallback,
-                                      aDiagnostics);
+  mDecoder = mPDM->CreateVideoDecoder({
+    mNeedAVCC ? mCurrentConfig : mOriginalConfig,
+    mTaskQueue,
+    mCallback,
+    aDiagnostics,
+    mImageContainer,
+    mLayersBackend
+  });
+
   if (!mDecoder) {
     mLastError = NS_ERROR_FAILURE;
     return NS_ERROR_FAILURE;
   }
   return NS_OK;
 }
 
 nsresult
--- a/dom/media/platforms/wrappers/H264Converter.h
+++ b/dom/media/platforms/wrappers/H264Converter.h
@@ -17,22 +17,17 @@ namespace mozilla {
 // provided in the init segment (e.g. AVC3 or Annex B)
 // H264Converter will monitor the input data, and will delay creation of the
 // MediaDataDecoder until a SPS and PPS NALs have been extracted.
 
 class H264Converter : public MediaDataDecoder {
 public:
 
   H264Converter(PlatformDecoderModule* aPDM,
-                const VideoInfo& aConfig,
-                layers::LayersBackend aLayersBackend,
-                layers::ImageContainer* aImageContainer,
-                TaskQueue* aTaskQueue,
-                MediaDataDecoderCallback* aCallback,
-                DecoderDoctorDiagnostics* aDiagnostics);
+                const CreateDecoderParams& aParams);
   virtual ~H264Converter();
 
   RefPtr<InitPromise> Init() override;
   nsresult Input(MediaRawData* aSample) override;
   nsresult Flush() override;
   nsresult Drain() override;
   nsresult Shutdown() override;
   bool IsHardwareAccelerated(nsACString& aFailureReason) const override;
--- a/dom/media/tests/mochitest/test_getUserMedia_constraints.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_constraints.html
@@ -42,16 +42,22 @@ var tests = [
     constraint: "mediaSource" },
   { message: "unknown mediaSource in audio fails",
     constraints: { audio: { mediaSource: 'uncle' } },
     error: "OverconstrainedError",
     constraint: "mediaSource" },
   { message: "emtpy constraint fails",
     constraints: { },
     error: "NotSupportedError" },
+  { message: "Triggering mock failure in default video device fails",
+    constraints: { video: { deviceId: 'bad device' }, fake: true },
+    error: "NotReadableError" },
+  { message: "Triggering mock failure in default audio device fails",
+    constraints: { audio: { deviceId: 'bad device' }, fake: true },
+    error: "NotReadableError" },
   { message: "Success-path: optional video facingMode + audio ignoring facingMode",
     constraints: { audio: { mediaSource: 'microphone',
                             facingMode: 'left',
                             foo: 0,
                             advanced: [{ facingMode: 'environment' },
                                        { facingMode: 'user' },
                                        { bar: 0 }] },
                    video: { mediaSource: 'camera',
--- a/dom/media/webrtc/MediaEngineDefault.cpp
+++ b/dom/media/webrtc/MediaEngineDefault.cpp
@@ -90,16 +90,22 @@ MediaEngineDefaultVideoSource::Allocate(
                                         const MediaEnginePrefs &aPrefs,
                                         const nsString& aDeviceId,
                                         const nsACString& aOrigin)
 {
   if (mState != kReleased) {
     return NS_ERROR_FAILURE;
   }
 
+  // Mock failure for automated tests.
+  if (aConstraints.mDeviceId.IsString() &&
+      aConstraints.mDeviceId.GetAsString().EqualsASCII("bad device")) {
+    return NS_ERROR_FAILURE;
+  }
+
   mOpts = aPrefs;
   mOpts.mWidth = mOpts.mWidth ? mOpts.mWidth : MediaEngine::DEFAULT_43_VIDEO_WIDTH;
   mOpts.mHeight = mOpts.mHeight ? mOpts.mHeight : MediaEngine::DEFAULT_43_VIDEO_HEIGHT;
   mState = kAllocated;
   return NS_OK;
 }
 
 nsresult
@@ -403,16 +409,22 @@ MediaEngineDefaultAudioSource::Allocate(
                                         const MediaEnginePrefs &aPrefs,
                                         const nsString& aDeviceId,
                                         const nsACString& aOrigin)
 {
   if (mState != kReleased) {
     return NS_ERROR_FAILURE;
   }
 
+  // Mock failure for automated tests.
+  if (aConstraints.mDeviceId.IsString() &&
+      aConstraints.mDeviceId.GetAsString().EqualsASCII("bad device")) {
+    return NS_ERROR_FAILURE;
+  }
+
   mState = kAllocated;
   // generate sine wave (default 1KHz)
   mSineGenerator = new SineWaveGenerator(AUDIO_RATE,
                                          static_cast<uint32_t>(aPrefs.mFreq ? aPrefs.mFreq : 1000));
   return NS_OK;
 }
 
 nsresult
--- a/dom/moz.build
+++ b/dom/moz.build
@@ -26,17 +26,16 @@ interfaces = [
     'json',
     'offline',
     'geolocation',
     'notification',
     'permission',
     'svg',
     'smil',
     'apps',
-    'gamepad',
     'push',
 ]
 
 DIRS += ['interfaces/' + i for i in interfaces]
 
 DIRS += [
     'animation',
     'apps',
@@ -109,16 +108,17 @@ DIRS += [
     'xul',
     'resourcestats',
     'manifest',
     'vr',
     'newapps',
     'u2f',
     'console',
     'performance',
+    'xhr',
 ]
 
 if CONFIG['OS_ARCH'] == 'WINNT':
     DIRS += ['plugins/ipc/hangui']
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk':
     DIRS += [
         'speakermanager',
--- a/dom/plugins/ipc/PluginInstanceChild.cpp
+++ b/dom/plugins/ipc/PluginInstanceChild.cpp
@@ -4628,10 +4628,16 @@ PluginInstanceChild::AnswerNPP_Destroy(N
     Destroy();
 
     return true;
 }
 
 void
 PluginInstanceChild::ActorDestroy(ActorDestroyReason why)
 {
+#ifdef XP_WIN
+    // ClearAllSurfaces() should not try to send anything after ActorDestroy.
+    mCurrentSurfaceActor = nullptr;
+    mBackSurfaceActor = nullptr;
+#endif
+
     Destroy();
 }
--- a/dom/push/PushDB.jsm
+++ b/dom/push/PushDB.jsm
@@ -120,20 +120,20 @@ this.PushDB.prototype = {
    */
   delete: function(aKeyID) {
     console.debug("delete()");
 
     return new Promise((resolve, reject) =>
       this.newTxn(
         "readwrite",
         this._dbStoreName,
-        function txnCb(aTxn, aStore) {
+        (aTxn, aStore) => {
           console.debug("delete: Removing record", aKeyID);
           aStore.get(aKeyID).onsuccess = event => {
-            aTxn.result = event.target.result;
+            aTxn.result = this.toPushRecord(event.target.result);
             aStore.delete(aKeyID);
           };
         },
         resolve,
         reject
       )
     );
   },
--- a/dom/push/PushNotifier.cpp
+++ b/dom/push/PushNotifier.cpp
@@ -42,55 +42,60 @@ NS_IMPL_CYCLE_COLLECTING_ADDREF(PushNoti
 NS_IMPL_CYCLE_COLLECTING_RELEASE(PushNotifier)
 
 NS_IMETHODIMP
 PushNotifier::NotifyPushWithData(const nsACString& aScope,
                                  nsIPrincipal* aPrincipal,
                                  const nsAString& aMessageId,
                                  uint32_t aDataLen, uint8_t* aData)
 {
+  NS_ENSURE_ARG(aPrincipal);
   nsTArray<uint8_t> data;
   if (!data.SetCapacity(aDataLen, fallible)) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
   if (!data.InsertElementsAt(0, aData, aDataLen, fallible)) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
   PushMessageDispatcher dispatcher(aScope, aPrincipal, aMessageId, Some(data));
   return Dispatch(dispatcher);
 }
 
 NS_IMETHODIMP
 PushNotifier::NotifyPush(const nsACString& aScope, nsIPrincipal* aPrincipal,
                          const nsAString& aMessageId)
 {
+  NS_ENSURE_ARG(aPrincipal);
   PushMessageDispatcher dispatcher(aScope, aPrincipal, aMessageId, Nothing());
   return Dispatch(dispatcher);
 }
 
 NS_IMETHODIMP
 PushNotifier::NotifySubscriptionChange(const nsACString& aScope,
                                        nsIPrincipal* aPrincipal)
 {
+  NS_ENSURE_ARG(aPrincipal);
   PushSubscriptionChangeDispatcher dispatcher(aScope, aPrincipal);
   return Dispatch(dispatcher);
 }
 
 NS_IMETHODIMP
 PushNotifier::NotifySubscriptionModified(const nsACString& aScope,
                                          nsIPrincipal* aPrincipal)
 {
+  NS_ENSURE_ARG(aPrincipal);
   PushSubscriptionModifiedDispatcher dispatcher(aScope, aPrincipal);
   return Dispatch(dispatcher);
 }
 
 NS_IMETHODIMP
 PushNotifier::NotifyError(const nsACString& aScope, nsIPrincipal* aPrincipal,
                           const nsAString& aMessage, uint32_t aFlags)
 {
+  NS_ENSURE_ARG(aPrincipal);
   PushErrorDispatcher dispatcher(aScope, aPrincipal, aMessage, aFlags);
   return Dispatch(dispatcher);
 }
 
 nsresult
 PushNotifier::Dispatch(PushDispatcher& aDispatcher)
 {
   if (XRE_IsParentProcess()) {
@@ -271,16 +276,19 @@ PushDispatcher::NotifyObserversAndWorker
 {
   Unused << NS_WARN_IF(NS_FAILED(NotifyObservers()));
   return NotifyWorkers();
 }
 
 bool
 PushDispatcher::ShouldNotifyWorkers()
 {
+  if (NS_WARN_IF(!mPrincipal)) {
+    return false;
+  }
   // System subscriptions use observer notifications instead of service worker
   // events. The `testing.notifyWorkers` pref disables worker events for
   // non-system subscriptions.
   return !nsContentUtils::IsSystemPrincipal(mPrincipal) &&
          Preferences::GetBool("dom.push.testing.notifyWorkers", true);
 }
 
 nsresult
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -1018,20 +1018,18 @@ this.PushService = {
 
   getAllUnexpired: function() {
     return this._db.getAllUnexpired();
   },
 
   _sendRequest(action, ...params) {
     if (this._state == PUSH_SERVICE_CONNECTION_DISABLE) {
       return Promise.reject(new Error("Push service disabled"));
-    } else if (this._state == PUSH_SERVICE_ACTIVE_OFFLINE) {
-      if (this._service.serviceType() == "WebSocket" && action == "unregister") {
-        return Promise.resolve();
-      }
+    }
+    if (this._state == PUSH_SERVICE_ACTIVE_OFFLINE) {
       return Promise.reject(new Error("Push service offline"));
     }
     // Ensure the backend is ready. `getByPageRecord` already checks this, but
     // we need to check again here in case the service was restarted in the
     // meantime.
     return this._checkActivated().then(_ => {
       switch (action) {
         case "register":
@@ -1198,22 +1196,23 @@ this.PushService = {
       .then(record => {
         if (record === undefined) {
           return false;
         }
 
         let reason = Ci.nsIPushErrorReporter.UNSUBSCRIBE_MANUAL;
         return Promise.all([
           this._sendUnregister(record, reason),
-          this._db.delete(record.keyID),
-        ]).then(() => {
-          gPushNotifier.notifySubscriptionModified(record.scope,
-                                                   record.principal);
-          return true;
-        });
+          this._db.delete(record.keyID).then(record => {
+            if (record) {
+              gPushNotifier.notifySubscriptionModified(record.scope,
+                                                       record.principal);
+            }
+          }),
+        ]).then(([success]) => success);
       });
   },
 
   clear: function(info) {
     return this._checkActivated()
       .then(_ => {
         return this._dropRegistrationsIf(record =>
           info.domain == "*" ||
--- a/dom/push/PushServiceAndroidGCM.jsm
+++ b/dom/push/PushServiceAndroidGCM.jsm
@@ -54,20 +54,16 @@ this.PushServiceAndroidGCM = {
   newPushDB: function() {
     return new PushDB(kPUSHANDROIDGCMDB_DB_NAME,
                       kPUSHANDROIDGCMDB_DB_VERSION,
                       kPUSHANDROIDGCMDB_STORE_NAME,
                       "channelID",
                       PushRecordAndroidGCM);
   },
 
-  serviceType: function() {
-    return "AndroidGCM";
-  },
-
   validServerURI: function(serverURI) {
     if (!serverURI) {
       return false;
     }
 
     if (serverURI.scheme == "https") {
       return true;
     }
--- a/dom/push/PushServiceHttp2.jsm
+++ b/dom/push/PushServiceHttp2.jsm
@@ -425,20 +425,16 @@ this.PushServiceHttp2 = {
   newPushDB: function() {
     return new PushDB(kPUSHHTTP2DB_DB_NAME,
                       kPUSHHTTP2DB_DB_VERSION,
                       kPUSHHTTP2DB_STORE_NAME,
                       "subscriptionUri",
                       PushRecordHttp2);
   },
 
-  serviceType: function() {
-    return "http2";
-  },
-
   hasmainPushService: function() {
     return this._mainPushService !== null;
   },
 
   validServerURI: function(serverURI) {
     if (serverURI.scheme == "http") {
       return !!prefs.get("testing.allowInsecureServerURL");
     }
--- a/dom/push/PushServiceWebSocket.jsm
+++ b/dom/push/PushServiceWebSocket.jsm
@@ -138,20 +138,16 @@ this.PushServiceWebSocket = {
   newPushDB: function() {
     return new PushDB(kPUSHWSDB_DB_NAME,
                       kPUSHWSDB_DB_VERSION,
                       kPUSHWSDB_STORE_NAME,
                       "channelID",
                       PushRecordWebSocket);
   },
 
-  serviceType: function() {
-    return "WebSocket";
-  },
-
   disconnect: function() {
     this._shutdownWS();
   },
 
   observe: function(aSubject, aTopic, aData) {
     if (aTopic == "nsPref:changed" && aData == "dom.push.userAgentID") {
       this._onUAIDChanged();
     } else if (aTopic == "timer-callback") {
@@ -229,27 +225,25 @@ this.PushServiceWebSocket = {
 
     if (this._lastPingTime > 0 &&
         now - this._lastPingTime > this._requestTimeout) {
 
       console.debug("timeOutRequests: Did not receive pong in time");
       requestTimedOut = true;
 
     } else {
-      for (let [channelID, request] of this._registerRequests) {
+      for (let [key, request] of this._pendingRequests) {
         let duration = now - request.ctime;
         // If any of the registration requests time out, all the ones after it
         // also made to fail, since we are going to be disconnecting the
         // socket.
         requestTimedOut |= duration > this._requestTimeout;
         if (requestTimedOut) {
-          request.reject(new Error(
-            "Register request timed out for channel ID " + channelID));
-
-          this._registerRequests.delete(channelID);
+          request.reject(new Error("Request timed out: " + key));
+          this._pendingRequests.delete(key);
         }
       }
     }
 
     // The most likely reason for a pong or registration request timing out is
     // that the socket has disconnected. Best to reconnect.
     if (requestTimedOut) {
       this._reconnect();
@@ -273,17 +267,17 @@ this.PushServiceWebSocket = {
         "Not updating userAgentID");
       return;
     }
     console.debug("New _UAID", newID);
     prefs.set("userAgentID", newID);
   },
 
   _ws: null,
-  _registerRequests: new Map(),
+  _pendingRequests: new Map(),
   _currentState: STATE_SHUT_DOWN,
   _requestTimeout: 0,
   _requestTimeoutTimer: null,
   _retryFailCount: 0,
 
   /**
    * According to the WS spec, servers should immediately close the underlying
    * TCP connection after they close a WebSocket. This causes wsOnStop to be
@@ -371,17 +365,17 @@ this.PushServiceWebSocket = {
 
     this._lastPingTime = 0;
 
     if (this._pingTimer) {
       this._pingTimer.cancel();
     }
 
     if (shouldCancelPending) {
-      this._cancelRegisterRequests();
+      this._cancelPendingRequests();
     }
 
     if (this._notifyRequestQueue) {
       this._notifyRequestQueue();
       this._notifyRequestQueue = null;
     }
   },
 
@@ -432,17 +426,17 @@ this.PushServiceWebSocket = {
       this._backoffTimer = Cc["@mozilla.org/timer;1"]
                                .createInstance(Ci.nsITimer);
     }
     this._backoffTimer.init(this, retryTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
   },
 
   /** Indicates whether we're waiting for pongs or requests. */
   _hasPendingRequests() {
-    return this._lastPingTime > 0 || this._registerRequests.size > 0;
+    return this._lastPingTime > 0 || this._pendingRequests.size > 0;
   },
 
   /**
    * Starts the request timeout timer unless we're already waiting for a pong
    * or register request.
    */
   _startRequestTimeoutTimer() {
     if (this._hasPendingRequests()) {
@@ -617,17 +611,17 @@ this.PushServiceWebSocket = {
       return;
     }
 
     let sendRequests = () => {
       if (this._notifyRequestQueue) {
         this._notifyRequestQueue();
         this._notifyRequestQueue = null;
       }
-      this._sendRegisterRequests();
+      this._sendPendingRequests();
     };
 
     function finishHandshake() {
       this._UAID = reply.uaid;
       this._currentState = STATE_READY;
       prefs.observe("userAgentID", this);
 
       this._dataEnabled = !!reply.use_webpush;
@@ -664,27 +658,22 @@ this.PushServiceWebSocket = {
     finishHandshake.bind(this)();
   },
 
   /**
    * Protocol handler invoked by server message.
    */
   _handleRegisterReply: function(reply) {
     console.debug("handleRegisterReply()");
-    if (typeof reply.channelID !== "string" ||
-        !this._registerRequests.has(reply.channelID)) {
+
+    let tmp = this._takeRequestForReply(reply);
+    if (!tmp) {
       return;
     }
 
-    let tmp = this._registerRequests.get(reply.channelID);
-    this._registerRequests.delete(reply.channelID);
-    if (!this._hasPendingRequests()) {
-      this._requestTimeoutTimer.cancel();
-    }
-
     if (reply.status == 200) {
       try {
         Services.io.newURI(reply.pushEndpoint, null, null);
       }
       catch (e) {
         tmp.reject(new Error("Invalid push endpoint: " + reply.pushEndpoint));
         return;
       }
@@ -703,16 +692,28 @@ this.PushServiceWebSocket = {
       tmp.resolve(record);
     } else {
       console.error("handleRegisterReply: Unexpected server response", reply);
       tmp.reject(new Error("Wrong status code for register reply: " +
         reply.status));
     }
   },
 
+  _handleUnregisterReply(reply) {
+    console.debug("handleUnregisterReply()");
+
+    let request = this._takeRequestForReply(reply);
+    if (!request) {
+      return;
+    }
+
+    let success = reply.status === 200;
+    request.resolve(success);
+  },
+
   _handleDataUpdate: function(update) {
     let promise;
     if (typeof update.channelID != "string") {
       console.warn("handleDataUpdate: Discarding update without channel ID",
         update);
       return;
     }
     function updateRecord(record) {
@@ -840,112 +841,131 @@ this.PushServiceWebSocket = {
                           .getService(Ci.nsIUUIDGenerator);
     // generateUUID() gives a UUID surrounded by {...}, slice them off.
     return uuidGenerator.generateUUID().toString().slice(1, -1);
   },
 
   register(record) {
     console.debug("register() ", record);
 
-    // start the timer since we now have at least one request
-    this._startRequestTimeoutTimer();
-
     let data = {channelID: this._generateID(),
                 messageType: "register"};
 
     if (record.appServerKey) {
       data.key = ChromeUtils.base64URLEncode(record.appServerKey, {
         // The Push server requires padding.
         pad: true,
       });
     }
 
-    return new Promise((resolve, reject) => {
-      this._registerRequests.set(data.channelID, {
-        record: record,
-        resolve: resolve,
-        reject: reject,
-        ctime: Date.now(),
-      });
-      this._queueRequest(data);
-    }).then(record => {
+    return this._sendRequestForReply(record, data).then(record => {
       if (!this._dataEnabled) {
         return record;
       }
       return PushCrypto.generateKeys()
         .then(([publicKey, privateKey]) => {
           record.p256dhPublicKey = publicKey;
           record.p256dhPrivateKey = privateKey;
           record.authenticationSecret = PushCrypto.generateAuthenticationSecret();
           return record;
         });
     });
   },
 
   unregister(record, reason) {
     console.debug("unregister() ", record, reason);
 
-    let code = kUNREGISTER_REASON_TO_CODE[reason];
-    if (!code) {
-      return Promise.reject(new Error('Invalid unregister reason'));
<