Bug 1016162 - Base EME DOM APIs, not connected to a CDM yet. r=bz
authorChris Pearce <cpearce@mozilla.com>
Sat, 07 Jun 2014 08:52:15 +1200
changeset 187333 e2b9d289514f019a8c6f9ca1e8fe8ef928a771fe
parent 187332 eca7bdeb0c6e333669cbbe585fbc3de979762aba
child 187334 758bbf88e5a890023e1ad22e39b525a1ea743feb
push id26917
push userryanvm@gmail.com
push dateSat, 07 Jun 2014 18:13:47 +0000
treeherdermozilla-central@a2f0e0619332 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz
bugs1016162
milestone32.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1016162 - Base EME DOM APIs, not connected to a CDM yet. r=bz
browser/app/profile/firefox.js
content/base/src/nsGkAtomList.h
content/html/content/public/HTMLMediaElement.h
content/html/content/src/HTMLMediaElement.cpp
content/html/content/src/HTMLSourceElement.h
content/media/eme/CDMProxy.cpp
content/media/eme/CDMProxy.h
content/media/eme/EMELog.cpp
content/media/eme/EMELog.h
content/media/eme/MediaKeyError.cpp
content/media/eme/MediaKeyError.h
content/media/eme/MediaKeyMessageEvent.cpp
content/media/eme/MediaKeyMessageEvent.h
content/media/eme/MediaKeyNeededEvent.cpp
content/media/eme/MediaKeyNeededEvent.h
content/media/eme/MediaKeySession.cpp
content/media/eme/MediaKeySession.h
content/media/eme/MediaKeys.cpp
content/media/eme/MediaKeys.h
content/media/eme/moz.build
content/media/moz.build
dom/events/EventNameList.h
dom/webidl/HTMLMediaElement.webidl
dom/webidl/HTMLSourceElement.webidl
dom/webidl/MediaKeyError.webidl
dom/webidl/MediaKeyMessageEvent.webidl
dom/webidl/MediaKeyNeededEvent.webidl
dom/webidl/MediaKeySession.webidl
dom/webidl/MediaKeys.webidl
dom/webidl/moz.build
dom/workers/WorkerFeature.h
parser/html/nsHtml5AtomList.h
widget/BasicEvents.h
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1560,16 +1560,18 @@ pref("identity.fxaccounts.remote.signin.
 // "identity.fxaccounts.remote.signup.uri" pref.
 pref("identity.fxaccounts.settings.uri", "https://accounts.firefox.com/settings");
 
 // On GTK, we now default to showing the menubar only when alt is pressed:
 #ifdef MOZ_WIDGET_GTK
 pref("ui.key.menuAccessKeyFocuses", true);
 #endif
 
+// Encrypted media extensions.
+pref("media.eme.enabled", false);
 
 // Delete HTTP cache v2 data of users that didn't opt-in manually
 pref("browser.cache.auto_delete_cache_version", 1);
 // Play with different values of the decay time and get telemetry,
 // 0 means to randomize (and persist) the experiment value in users' profiles,
 // -1 means no experiment is run and we use the preferred value for frecency (6h)
 pref("browser.cache.frecency_experiment", 0);
 
--- a/content/base/src/nsGkAtomList.h
+++ b/content/base/src/nsGkAtomList.h
@@ -492,16 +492,17 @@ GK_ATOM(noautofocus, "noautofocus")
 GK_ATOM(keepcurrentinview, "keepcurrentinview")
 GK_ATOM(keepobjectsalive, "keepobjectsalive")
 GK_ATOM(key, "key")
 GK_ATOM(keycode, "keycode")
 GK_ATOM(keydown, "keydown")
 GK_ATOM(keygen, "keygen")
 GK_ATOM(keypress, "keypress")
 GK_ATOM(keyset, "keyset")
+GK_ATOM(keysystem, "keysystem")
 GK_ATOM(keytext, "keytext")
 GK_ATOM(keyup, "keyup")
 GK_ATOM(kind, "kind")
 GK_ATOM(label, "label")
 GK_ATOM(lang, "lang")
 GK_ATOM(language, "language")
 GK_ATOM(last, "last")
 GK_ATOM(layer, "layer")
@@ -1939,16 +1940,18 @@ GK_ATOM(onended, "onended")
 GK_ATOM(onratechange, "onratechange")
 GK_ATOM(ondurationchange, "ondurationchange")
 GK_ATOM(onvolumechange, "onvolumechange")
 GK_ATOM(onaddtrack, "onaddtrack")
 GK_ATOM(oncuechange, "oncuechange")
 GK_ATOM(oncurrentchange, "oncurrentchange")
 GK_ATOM(onenter, "onenter")
 GK_ATOM(onexit, "onexit")
+GK_ATOM(onneedkey, "onneedkey")
+GK_ATOM(needkey, "needkey")
 GK_ATOM(onremovetrack, "onremovetrack")
 GK_ATOM(loadstart, "loadstart")
 GK_ATOM(suspend, "suspend")
 GK_ATOM(emptied, "emptied")
 GK_ATOM(stalled, "stalled")
 GK_ATOM(play, "play")
 GK_ATOM(pause, "pause")
 GK_ATOM(loadedmetadata, "loadedmetadata")
--- a/content/html/content/public/HTMLMediaElement.h
+++ b/content/html/content/public/HTMLMediaElement.h
@@ -15,16 +15,26 @@
 #include "DOMMediaStream.h"
 #include "AudioChannelCommon.h"
 #include "DecoderTraits.h"
 #include "nsIAudioChannelAgent.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/dom/AudioChannelBinding.h"
 #include "mozilla/dom/TextTrackManager.h"
 #include "MediaDecoder.h"
+#include "mozilla/dom/MediaKeys.h"
+
+// Something on Linux #defines None, which is an entry in the
+// MediaWaitingFor enum, so undef it here before including the binfing,
+// so that the build doesn't fail...
+#ifdef None
+#undef None
+#endif
+
+#include "mozilla/dom/HTMLMediaElementBinding.h"
 
 // Define to output information on decoding and painting framerate
 /* #define DEBUG_FRAME_RATE 1 */
 
 class nsIChannel;
 class nsIHttpChannel;
 class nsILoadGroup;
 
@@ -32,16 +42,17 @@ typedef uint16_t nsMediaNetworkState;
 typedef uint16_t nsMediaReadyState;
 
 namespace mozilla {
 class ErrorResult;
 class MediaResource;
 class MediaDecoder;
 class VideoFrameContainer;
 namespace dom {
+class MediaKeys;
 class TextTrack;
 class TimeRanges;
 class WakeLock;
 }
 }
 
 class nsITimer;
 class nsRange;
@@ -473,16 +484,32 @@ public:
 
   bool MozPreservesPitch() const
   {
     return mPreservesPitch;
   }
 
   // XPCOM MozPreservesPitch() is OK
 
+  MediaKeys* GetMediaKeys() const;
+
+  already_AddRefed<Promise> SetMediaKeys(MediaKeys* mediaKeys,
+                                         ErrorResult& aRv);
+  
+  MediaWaitingFor WaitingFor() const;
+
+  mozilla::dom::EventHandlerNonNull* GetOnneedkey();
+  void SetOnneedkey(mozilla::dom::EventHandlerNonNull* listener);
+
+  void DispatchNeedKey(const nsTArray<uint8_t>& aInitData,
+                       const nsAString& aInitDataType);
+
+
+  bool IsEventAttributeName(nsIAtom* aName) MOZ_OVERRIDE;
+
   bool MozAutoplayEnabled() const
   {
     return mAutoplayEnabled;
   }
 
   already_AddRefed<DOMMediaStream> MozCaptureStream(ErrorResult& aRv);
 
   already_AddRefed<DOMMediaStream> MozCaptureStreamUntilEnded(ErrorResult& aRv);
@@ -1005,16 +1032,19 @@ protected:
 
   // Reference to the source element last returned by GetNextSource().
   // This is the child source element which we're trying to load from.
   nsCOMPtr<nsIContent> mSourceLoadCandidate;
 
   // Range of time played.
   nsRefPtr<TimeRanges> mPlayed;
 
+  // Encrypted Media Extension media keys.
+  nsRefPtr<MediaKeys> mMediaKeys;
+
   // Stores the time at the start of the current 'played' range.
   double mCurrentPlayRangeStart;
 
   // If true then we have begun downloading the media content.
   // Set to false when completed, or not yet started.
   bool mBegun;
 
   // True when the decoder has loaded enough data to display the
@@ -1134,14 +1164,16 @@ protected:
 
   // Is this media element playing?
   bool mPlayingThroughTheAudioChannel;
 
   // An agent used to join audio channel service.
   nsCOMPtr<nsIAudioChannelAgent> mAudioChannelAgent;
 
   nsRefPtr<TextTrackManager> mTextTrackManager;
+
+  MediaWaitingFor mWaitingFor;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_HTMLMediaElement_h
--- a/content/html/content/src/HTMLMediaElement.cpp
+++ b/content/html/content/src/HTMLMediaElement.cpp
@@ -4,16 +4,18 @@
  * 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/HTMLMediaElement.h"
 #include "mozilla/dom/HTMLMediaElementBinding.h"
 #include "mozilla/dom/ElementInlines.h"
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/MathAlgorithms.h"
+#include "mozilla/dom/MediaKeyNeededEvent.h"
+#include "mozilla/AsyncEventDispatcher.h"
 
 #include "base/basictypes.h"
 #include "nsIDOMHTMLMediaElement.h"
 #include "nsIDOMHTMLSourceElement.h"
 #include "TimeRanges.h"
 #include "nsGenericHTMLElement.h"
 #include "nsAttrValueInlines.h"
 #include "nsPresContext.h"
@@ -418,36 +420,38 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSourceLoadCandidate)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioChannelAgent)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mError)
   for (uint32_t i = 0; i < tmp->mOutputStreams.Length(); ++i) {
     NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputStreams[i].mStream);
   }
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPlayed);
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextTrackManager)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaKeys)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLMediaElement, nsGenericHTMLElement)
   if (tmp->mSrcStream) {
     // Need to EndMediaStreamPlayback to clear mSrcStream and make sure everything
     // gets unhooked correctly.
     tmp->EndSrcMediaStreamPlayback();
   }
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcAttrStream)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaSource)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourcePointer)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mLoadBlockedDoc)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourceLoadCandidate)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioChannelAgent)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mError)
   for (uint32_t i = 0; i < tmp->mOutputStreams.Length(); ++i) {
-    NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputStreams[i].mStream);
+    NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputStreams[i].mStream)
   }
-  NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlayed);
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlayed)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextTrackManager)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaKeys)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(HTMLMediaElement)
   NS_INTERFACE_MAP_ENTRY(nsIObserver)
   NS_INTERFACE_MAP_ENTRY(nsIAudioChannelAgentCallback)
 NS_INTERFACE_MAP_END_INHERITING(nsGenericHTMLElement)
 
 // nsIDOMHTMLMediaElement
@@ -890,16 +894,19 @@ void HTMLMediaElement::LoadFromSourceChi
     nsAutoString type;
     if (child->GetAttr(kNameSpaceID_None, nsGkAtoms::type, type) &&
         GetCanPlay(type) == CANPLAY_NO) {
       DispatchAsyncSourceError(child);
       const char16_t* params[] = { type.get(), src.get() };
       ReportLoadError("MediaLoadUnsupportedTypeAttribute", params, ArrayLength(params));
       continue;
     }
+    // TODO: "If candidate has a keySystem attribute whose value represents a
+    //       Key System that the user agent knows it cannot use with type,
+    //       then end the synchronous section[...]" (Bug 1016707)
     nsAutoString media;
     if (child->GetAttr(kNameSpaceID_None, nsGkAtoms::media, media) && !media.IsEmpty()) {
       nsCSSParser cssParser;
       nsRefPtr<nsMediaList> mediaList(new nsMediaList());
       cssParser.ParseMediaList(media, nullptr, 0, mediaList, false);
       nsIPresShell* presShell = OwnerDoc()->GetShell();
       if (presShell && !mediaList->Matches(presShell->GetPresContext(), nullptr)) {
         DispatchAsyncSourceError(child);
@@ -1998,17 +2005,18 @@ HTMLMediaElement::HTMLMediaElement(alrea
     mHasSelfReference(false),
     mShuttingDown(false),
     mSuspendedForPreloadNone(false),
     mMediaSecurityVerified(false),
     mCORSMode(CORS_NONE),
     mHasAudio(false),
     mDownloadSuspendedByCache(false),
     mAudioChannelFaded(false),
-    mPlayingThroughTheAudioChannel(false)
+    mPlayingThroughTheAudioChannel(false),
+    mWaitingFor(MediaWaitingFor::None)
 {
 #ifdef PR_LOGGING
   if (!gMediaElementLog) {
     gMediaElementLog = PR_NewLogModule("nsMediaElement");
   }
   if (!gMediaElementEventsLog) {
     gMediaElementEventsLog = PR_NewLogModule("nsMediaElementEvents");
   }
@@ -3889,16 +3897,82 @@ NS_IMETHODIMP HTMLMediaElement::CanPlayC
 
   NS_ENSURE_TRUE(nsContentUtils::IsCallerChrome(), NS_ERROR_NOT_AVAILABLE);
 
   UpdateChannelMuteState(static_cast<AudioChannelState>(canPlay));
   mPaused.SetCanPlay(canPlay != AUDIO_CHANNEL_STATE_MUTED);
   return NS_OK;
 }
 
+MediaKeys*
+HTMLMediaElement::GetMediaKeys() const
+{
+  return mMediaKeys;
+}
+
+already_AddRefed<Promise>
+HTMLMediaElement::SetMediaKeys(mozilla::dom::MediaKeys* aMediaKeys,
+                               ErrorResult& aRv)
+{
+  nsCOMPtr<nsIGlobalObject> global =
+    do_QueryInterface(OwnerDoc()->GetInnerWindow());
+  if (!global) {
+    aRv.Throw(NS_ERROR_UNEXPECTED);
+    return nullptr;
+  }
+  // TODO: Need to shutdown existing MediaKeys instance? bug 1016709.
+  nsRefPtr<Promise> promise = new Promise(global);
+  if (mMediaKeys != aMediaKeys) {
+    mMediaKeys = aMediaKeys;
+  }
+  promise->MaybeResolve(JS::UndefinedHandleValue);
+  return promise.forget();
+}
+
+MediaWaitingFor
+HTMLMediaElement::WaitingFor() const
+{
+  return mWaitingFor;
+}
+
+EventHandlerNonNull*
+HTMLMediaElement::GetOnneedkey()
+{
+  EventListenerManager *elm = GetExistingListenerManager();
+  return elm ? elm->GetEventHandler(nsGkAtoms::onneedkey, EmptyString())
+              : nullptr;
+}
+
+void
+HTMLMediaElement::SetOnneedkey(EventHandlerNonNull* handler)
+{
+  EventListenerManager *elm = GetOrCreateListenerManager();
+  if (elm) {
+    elm->SetEventHandler(nsGkAtoms::onneedkey, EmptyString(), handler);
+  }
+}
+
+void
+HTMLMediaElement::DispatchNeedKey(const nsTArray<uint8_t>& aInitData,
+                                  const nsAString& aInitDataType)
+{
+  nsRefPtr<MediaKeyNeededEvent> event(
+    MediaKeyNeededEvent::Constructor(this, aInitDataType, aInitData));
+  nsRefPtr<AsyncEventDispatcher> asyncDispatcher =
+    new AsyncEventDispatcher(this, event);
+  asyncDispatcher->PostDOMEvent();
+}
+
+bool
+HTMLMediaElement::IsEventAttributeName(nsIAtom* aName)
+{
+  return aName == nsGkAtoms::onneedkey ||
+         nsGenericHTMLElement::IsEventAttributeName(aName);
+}
+
 NS_IMETHODIMP HTMLMediaElement::WindowVolumeChanged()
 {
   SetVolumeInternal();
   return NS_OK;
 }
 
 /* readonly attribute TextTrackList textTracks; */
 TextTrackList*
--- a/content/html/content/src/HTMLSourceElement.h
+++ b/content/html/content/src/HTMLSourceElement.h
@@ -59,16 +59,26 @@ public:
   {
     GetHTMLAttr(nsGkAtoms::media, aMedia);
   }
   void SetMedia(const nsAString& aMedia, mozilla::ErrorResult& rv)
   {
     SetHTMLAttr(nsGkAtoms::media, aMedia, rv);
   }
 
+  void GetKeySystem(nsString& aKeySystem) const
+  {
+    GetHTMLAttr(nsGkAtoms::keysystem, aKeySystem);
+  }
+
+  void SetKeySystem(const nsAString& aKeySystem)
+  {
+    SetHTMLAttr(nsGkAtoms::keysystem, aKeySystem);
+  }
+
 protected:
   virtual JSObject* WrapNode(JSContext* aCx) MOZ_OVERRIDE;
 
 protected:
   virtual void GetItemValueText(nsAString& text) MOZ_OVERRIDE;
   virtual void SetItemValueText(const nsAString& text) MOZ_OVERRIDE;
 };
 
new file mode 100644
--- /dev/null
+++ b/content/media/eme/CDMProxy.cpp
@@ -0,0 +1,199 @@
+/* -*- 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/CDMProxy.h"
+#include "nsString.h"
+#include "mozilla/dom/MediaKeys.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozIGeckoMediaPluginService.h"
+#include "nsContentCID.h"
+#include "nsServiceManagerUtils.h"
+#include "MainThreadUtils.h"
+
+// TODO: Change the functions in this file to do IPC via the Gecko Media
+// Plugins API. In the meantime, the code here merely implements the
+// interface we expect will be required when the IPC is working.
+
+namespace mozilla {
+
+CDMProxy::CDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem)
+  : mKeys(aKeys)
+  , mKeySystem(aKeySystem)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_COUNT_CTOR(CDMProxy);
+}
+
+CDMProxy::~CDMProxy()
+{
+  MOZ_COUNT_DTOR(CDMProxy);
+}
+
+void
+CDMProxy::Init(PromiseId aPromiseId)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  if (!mGMPThread) {
+    nsCOMPtr<mozIGeckoMediaPluginService> mps =
+      do_GetService("@mozilla.org/gecko-media-plugin-service;1");
+    if (!mps) {
+      RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
+      return;
+    }
+    mps->GetThread(getter_AddRefs(mGMPThread));
+    if (!mGMPThread) {
+      RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
+      return;
+    }
+  }
+
+  // TODO: Dispatch task to GMPThread to initialize CDM via IPC.
+
+  mKeys->OnCDMCreated(aPromiseId);
+}
+
+static int sFakeSessionIdNum = 0;
+
+void
+CDMProxy::CreateSession(dom::SessionType aSessionType,
+                        PromiseId aPromiseId,
+                        const nsAString& aInitDataType,
+                        const Uint8Array& aInitData)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(mGMPThread);
+
+  // TODO: Dispatch task to GMPThread to call CDM CreateSession via IPC.
+
+  // Make a fake session id. We'll get this from the CDM normally.
+  nsAutoString id;
+  id.AppendASCII("FakeSessionId_");
+  id.AppendInt(sFakeSessionIdNum++);
+
+  mKeys->OnSessionActivated(aPromiseId, id);
+}
+
+void
+CDMProxy::LoadSession(PromiseId aPromiseId,
+                      const nsAString& aSessionId)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(mGMPThread);
+
+  // TODO: Dispatch task to GMPThread to call CDM LoadSession via IPC.
+  // make MediaKeys::mPendingSessions CC'd
+
+  mKeys->OnSessionActivated(aPromiseId, aSessionId);
+}
+
+void
+CDMProxy::SetServerCertificate(PromiseId aPromiseId,
+                               const Uint8Array& aCertData)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(mGMPThread);
+
+  // TODO: Dispatch task to GMPThread to call CDM SetServerCertificate via IPC.
+
+  ResolvePromise(aPromiseId);
+}
+
+static int sUpdateCount = 0;
+
+void
+CDMProxy::UpdateSession(const nsAString& aSessionId,
+                        PromiseId aPromiseId,
+                        const Uint8Array& aResponse)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(mGMPThread);
+  NS_ENSURE_TRUE_VOID(!mKeys.IsNull());
+
+  // TODO: Dispatch task to GMPThread to call CDM UpdateSession via IPC.
+
+  nsRefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+  nsAutoCString str(NS_LITERAL_CSTRING("Update_"));
+  str.AppendInt(sUpdateCount++);
+  nsTArray<uint8_t> msg;
+  msg.AppendElements(str.get(), str.Length());
+  session->DispatchKeyMessage(msg, NS_LITERAL_STRING("http://bogus.url"));
+  ResolvePromise(aPromiseId);
+}
+
+void
+CDMProxy::CloseSession(const nsAString& aSessionId,
+                       PromiseId aPromiseId)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  NS_ENSURE_TRUE_VOID(!mKeys.IsNull());
+
+  // TODO: Dispatch task to GMPThread to call CDM CloseSession via IPC.
+
+  nsRefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+
+  // Pretend that the CDM actually does close the session...
+  // "...the [MediaKeySession's] closed attribute promise is resolved
+  // when the session is closed."
+  session->OnClosed();
+
+  // "The promise is resolved when the request has been processed."
+  ResolvePromise(aPromiseId);
+}
+
+void
+CDMProxy::RemoveSession(const nsAString& aSessionId,
+                        PromiseId aPromiseId)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  // TODO: Dispatch task to GMPThread to call CDM RemoveSession via IPC.
+
+  // Assume CDM immediately removes session's data, then close the session
+  // as per the spec.
+  CloseSession(aSessionId, aPromiseId);
+}
+
+void
+CDMProxy::Shutdown()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  mKeys.Clear();
+}
+
+void
+CDMProxy::RejectPromise(PromiseId aId, nsresult aCode)
+{
+  if (NS_IsMainThread()) {
+    if (!mKeys.IsNull()) {
+      mKeys->RejectPromise(aId, aCode);
+    } else {
+      NS_WARNING("CDMProxy unable to reject promise!");
+    }
+  } else {
+    nsRefPtr<nsIRunnable> task(new RejectPromiseTask(this, aId, aCode));
+    NS_DispatchToMainThread(task);
+  }
+}
+
+void
+CDMProxy::ResolvePromise(PromiseId aId)
+{
+  if (NS_IsMainThread()) {
+    if (!mKeys.IsNull()) {
+      mKeys->ResolvePromise(aId);
+    } else {
+      NS_WARNING("CDMProxy unable to resolve promise!");
+    }
+  } else {
+    nsRefPtr<nsIRunnable> task;
+    task = NS_NewRunnableMethodWithArg<PromiseId>(this,
+                                                  &CDMProxy::ResolvePromise,
+                                                  aId);
+    NS_DispatchToMainThread(task);
+  }
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/content/media/eme/CDMProxy.h
@@ -0,0 +1,171 @@
+/* -*- 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 CDMProxy_h_
+#define CDMProxy_h_
+
+#include "nsString.h"
+#include "nsAutoPtr.h"
+#include "nsProxyRelease.h"
+#include "mozilla/dom/MediaKeys.h"
+#include "mozilla/dom/TypedArray.h"
+
+class nsIThread;
+
+namespace mozilla {
+
+namespace dom {
+class MediaKeySession;
+}
+
+// A placeholder proxy to the CDM.
+// TODO: The functions here need to do IPC to talk to the CDM via the
+// Gecko Media Plugin API, which we'll need to extend for H.264 and EME
+// content.
+// Note: Promises are passed in via a PromiseId, so that the ID can be
+// passed via IPC to the CDM, which can then signal when to reject or
+// resolve the promise using its PromiseId.
+class CDMProxy {
+  typedef dom::PromiseId PromiseId;
+  typedef dom::SessionType SessionType;
+  typedef dom::Uint8Array Uint8Array;
+public:
+
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CDMProxy)
+
+  // Main thread only.
+  CDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem);
+
+  // Main thread only.
+  // Loads the CDM corresponding to mKeySystem.
+  // Calls MediaKeys::OnCDMCreated() when the CDM is created.
+  void Init(PromiseId aPromiseId);
+
+  // Main thread only.
+  // Uses the CDM to create a key session.
+  // Caller is responsible for calling aInitData.ComputeLengthAndData().
+  // Calls MediaKeys::OnSessionActivated() when session is created.
+  void CreateSession(dom::SessionType aSessionType,
+                     PromiseId aPromiseId,
+                     const nsAString& aInitDataType,
+                     const Uint8Array& aInitData);
+
+  // Main thread only.
+  // Uses the CDM to load a presistent session stored on disk.
+  // Calls MediaKeys::OnSessionActivated() when session is loaded.
+  void LoadSession(PromiseId aPromiseId,
+                   const nsAString& aSessionId);
+
+  // Main thread only.
+  // Sends a new certificate to the CDM.
+  // Caller is responsible for calling aCert.ComputeLengthAndData().
+  // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has
+  // processed the request.
+  void SetServerCertificate(PromiseId aPromiseId,
+                            const Uint8Array& aCert);
+
+  // Main thread only.
+  // Sends an update to the CDM.
+  // Caller is responsible for calling aResponse.ComputeLengthAndData().
+  // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has
+  // processed the request.
+  void UpdateSession(const nsAString& aSessionId,
+                     PromiseId aPromiseId,
+                     const Uint8Array& aResponse);
+
+  // Main thread only.
+  // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has
+  // processed the request.
+  // If processing this operation results in the session actually closing,
+  // we also call MediaKeySession::OnClosed(), which in turn calls
+  // MediaKeys::OnSessionClosed().
+  void CloseSession(const nsAString& aSessionId,
+                    PromiseId aPromiseId);
+
+  // Main thread only.
+  // Removes all data for a persisent session.
+  // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has
+  // processed the request.
+  void RemoveSession(const nsAString& aSessionId,
+                     PromiseId aPromiseId);
+
+  // Main thread only.
+  void Shutdown();
+
+private:
+
+  class RejectPromiseTask : public nsRunnable {
+  public:
+    RejectPromiseTask(CDMProxy* aProxy,
+                      PromiseId aId,
+                      nsresult aCode)
+      : mProxy(aProxy)
+      , mId(aId)
+      , mCode(aCode)
+    {
+    }
+    NS_METHOD Run() {
+      mProxy->RejectPromise(mId, mCode);
+      return NS_OK;
+    }
+  private:
+    nsRefPtr<CDMProxy> mProxy;
+    PromiseId mId;
+    nsresult mCode;
+  };
+
+  // Reject promise with DOMException corresponding to aExceptionCode.
+  // Can be called from any thread.
+  void RejectPromise(PromiseId aId, nsresult aExceptionCode);
+  // Resolves promise with "undefined".
+  // Can be called from any thread.
+  void ResolvePromise(PromiseId aId);
+
+  ~CDMProxy();
+
+  // Helper to enforce that a raw pointer is only accessed on the main thread.
+  template<class Type>
+  class MainThreadOnlyRawPtr {
+  public:
+    MainThreadOnlyRawPtr(Type* aPtr)
+      : mPtr(aPtr)
+    {
+      MOZ_ASSERT(NS_IsMainThread());
+    }
+
+    bool IsNull() const {
+      MOZ_ASSERT(NS_IsMainThread());
+      return !mPtr;
+    }
+
+    void Clear() {
+      MOZ_ASSERT(NS_IsMainThread());
+      mPtr = nullptr;
+    }
+
+    Type* operator->() const {
+      MOZ_ASSERT(NS_IsMainThread());
+      return mPtr;
+    }
+  private:
+    Type* mPtr;
+  };
+
+  // Our reference back to the MediaKeys object.
+  // WARNING: This is a non-owning reference that is cleared by MediaKeys
+  // destructor. only use on main thread, and always nullcheck before using!
+  MainThreadOnlyRawPtr<dom::MediaKeys> mKeys;
+
+  const nsAutoString mKeySystem;
+
+  // Gecko Media Plugin thread. All interactions with the out-of-process
+  // EME plugin must come from this thread.
+  nsRefPtr<nsIThread> mGMPThread;
+};
+
+} // namespace mozilla
+
+#endif // CDMProxy_h_
new file mode 100644
--- /dev/null
+++ b/content/media/eme/EMELog.cpp
@@ -0,0 +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 "EMELog.h"
+#include "mozilla/NullPtr.h"
+
+namespace mozilla {
+
+#ifdef PR_LOGGING
+
+PRLogModuleInfo* GetEMELog() {
+  static PRLogModuleInfo* log = nullptr;
+  if (!log) {
+    log = PR_NewLogModule("EME");
+  }
+  return log;
+}
+
+PRLogModuleInfo* GetEMEVerboseLog() {
+  static PRLogModuleInfo* log = nullptr;
+  if (!log) {
+    log = PR_NewLogModule("EMEV");
+  }
+  return log;
+}
+
+#endif
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/content/media/eme/EMELog.h
@@ -0,0 +1,35 @@
+/* -*- 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 "prlog.h"
+
+namespace mozilla {
+
+#ifdef PR_LOGGING
+
+#ifndef EME_LOG
+PRLogModuleInfo* GetEMELog();
+#define EME_LOG(...) PR_LOG(GetEMELog(), PR_LOG_DEBUG, (__VA_ARGS__))
+#endif
+
+#ifndef EME_VERBOSE_LOG
+PRLogModuleInfo* GetEMEVerboseLog();
+#define EME_VERBOSE_LOG(...) PR_LOG(GetEMEVerboseLog(), PR_LOG_DEBUG, (__VA_ARGS__))
+
+#else
+
+#ifndef EME_LOG
+#define EME_LOG(...)
+#endif
+
+#ifndef EME_VERBOSE_LOG
+#define EME_VERBOSE_LOG(...)
+#endif
+
+#endif
+
+#endif // PR_LOGGING
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/content/media/eme/MediaKeyError.cpp
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaKeyError.h"
+#include "mozilla/dom/MediaKeyErrorBinding.h"
+#include "nsContentUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+MediaKeyError::MediaKeyError(EventTarget* aOwner, uint32_t aSystemCode)
+  : Event(aOwner, nullptr, nullptr)
+  , mSystemCode(aSystemCode)
+{
+  SetIsDOMBinding();
+}
+
+MediaKeyError::~MediaKeyError()
+{
+}
+
+uint32_t
+MediaKeyError::SystemCode() const
+{
+  return mSystemCode;
+}
+
+JSObject*
+MediaKeyError::WrapObject(JSContext* aCx)
+{
+  return MediaKeyErrorBinding::Wrap(aCx, this);
+}
+
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/content/media/eme/MediaKeyError.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaKeyError_h
+#define mozilla_dom_MediaKeyError_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "nsWrapperCache.h"
+#include "mozilla/dom/Event.h"
+#include "js/TypeDecls.h"
+
+namespace mozilla {
+namespace dom {
+
+class MediaKeyError MOZ_FINAL : public Event
+{
+public:
+  NS_FORWARD_TO_EVENT
+
+  MediaKeyError(EventTarget* aOwner, uint32_t aSystemCode);
+  ~MediaKeyError();
+
+  virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE;
+
+  uint32_t SystemCode() const;
+
+private:
+  uint32_t mSystemCode;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/content/media/eme/MediaKeyMessageEvent.cpp
@@ -0,0 +1,127 @@
+/* -*- 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/MediaKeyMessageEvent.h"
+#include "mozilla/dom/MediaKeyMessageEventBinding.h"
+#include "js/GCAPI.h"
+#include "jsfriendapi.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/dom/PrimitiveConversions.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/dom/TypedArray.h"
+#include "nsContentUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaKeyMessageEvent)
+
+NS_IMPL_ADDREF_INHERITED(MediaKeyMessageEvent, Event)
+NS_IMPL_RELEASE_INHERITED(MediaKeyMessageEvent, Event)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaKeyMessageEvent, Event)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaKeyMessageEvent, Event)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mMessage)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaKeyMessageEvent, Event)
+  tmp->mMessage = nullptr;
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaKeyMessageEvent)
+NS_INTERFACE_MAP_END_INHERITING(Event)
+
+MediaKeyMessageEvent::MediaKeyMessageEvent(EventTarget* aOwner)
+  : Event(aOwner, nullptr, nullptr)
+{
+  mozilla::HoldJSObjects(this);
+}
+
+MediaKeyMessageEvent::~MediaKeyMessageEvent()
+{
+  mMessage = nullptr;
+  mozilla::DropJSObjects(this);
+}
+
+MediaKeyMessageEvent*
+MediaKeyMessageEvent::AsMediaKeyMessageEvent()
+{
+  return this;
+}
+
+JSObject*
+MediaKeyMessageEvent::WrapObject(JSContext* aCx)
+{
+  return MediaKeyMessageEventBinding::Wrap(aCx, this);
+}
+
+already_AddRefed<MediaKeyMessageEvent>
+MediaKeyMessageEvent::Constructor(EventTarget* aOwner,
+                                  const nsAString& aURL,
+                                  const nsTArray<uint8_t>& aMessage)
+{
+  nsRefPtr<MediaKeyMessageEvent> e = new MediaKeyMessageEvent(aOwner);
+  e->InitEvent(NS_LITERAL_STRING("message"), false, false);
+  e->mRawMessage = aMessage;
+  e->mDestinationURL = aURL;
+  e->SetTrusted(true);
+  return e.forget();
+}
+
+already_AddRefed<MediaKeyMessageEvent>
+MediaKeyMessageEvent::Constructor(const GlobalObject& aGlobal,
+                                  const nsAString& aType,
+                                  const MediaKeyMessageEventInit& aEventInitDict,
+                                  ErrorResult& aRv)
+{
+  nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports());
+  nsRefPtr<MediaKeyMessageEvent> e = new MediaKeyMessageEvent(owner);
+  bool trusted = e->Init(owner);
+  e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable);
+  if (aEventInitDict.mMessage.WasPassed()) {
+    const auto& a = aEventInitDict.mMessage.Value();
+    a.ComputeLengthAndData();
+    e->mMessage = Uint8Array::Create(aGlobal.GetContext(), owner, a.Length(), a.Data());
+  } else {
+    e->mMessage = Uint8Array::Create(aGlobal.GetContext(), owner, 0, nullptr);
+  }
+  if (!e->mMessage) {
+    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+    return nullptr;
+  }
+  e->mDestinationURL = aEventInitDict.mDestinationURL;
+  e->SetTrusted(trusted);
+  return e.forget();
+}
+
+JSObject*
+MediaKeyMessageEvent::GetMessage(JSContext* cx, ErrorResult& aRv)
+{
+  if (!mMessage) {
+    mMessage = Uint8Array::Create(cx,
+                                  this,
+                                  mRawMessage.Length(),
+                                  mRawMessage.Elements());
+    if (!mMessage) {
+      aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+      return nullptr;
+    }
+    mRawMessage.Clear();
+  }
+  JS::ExposeObjectToActiveJS(mMessage);
+  return mMessage;
+}
+
+void
+MediaKeyMessageEvent::GetDestinationURL(nsString& aRetVal) const
+{
+  aRetVal = mDestinationURL;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/content/media/eme/MediaKeyMessageEvent.h
@@ -0,0 +1,65 @@
+/* -*- 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_MediaKeyMessageEvent_h__
+#define mozilla_dom_MediaKeyMessageEvent_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+#include "nsCOMPtr.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/TypedArray.h"
+#include "js/TypeDecls.h"
+#include "mozilla/dom/MediaKeyMessageEventBinding.h"
+
+namespace mozilla {
+namespace dom {
+
+class MediaKeyMessageEventInit;
+
+class MediaKeyMessageEvent MOZ_FINAL : public Event
+{
+public:
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MediaKeyMessageEvent, Event)
+  virtual ~MediaKeyMessageEvent();
+protected:
+  MediaKeyMessageEvent(EventTarget* aOwner);
+
+  JS::Heap<JSObject*> mMessage;
+  nsString mDestinationURL;
+
+public:
+  virtual MediaKeyMessageEvent* AsMediaKeyMessageEvent();
+
+  virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE;
+
+  static already_AddRefed<MediaKeyMessageEvent>
+    Constructor(EventTarget* aOwner,
+                const nsAString& aURL,
+                const nsTArray<uint8_t>& aMessage);
+
+  static already_AddRefed<MediaKeyMessageEvent>
+  Constructor(const GlobalObject& aGlobal,
+              const nsAString& aType,
+              const MediaKeyMessageEventInit& aEventInitDict,
+              ErrorResult& aRv);
+
+  JSObject* GetMessage(JSContext* cx, ErrorResult& aRv);
+
+  void GetDestinationURL(nsString& aRetVal) const;
+
+private:
+  nsTArray<uint8_t> mRawMessage;
+};
+
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_MediaKeyMessageEvent_h__
new file mode 100644
--- /dev/null
+++ b/content/media/eme/MediaKeyNeededEvent.cpp
@@ -0,0 +1,118 @@
+/* -*- 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 "MediaKeyNeededEvent.h"
+#include "mozilla/dom/MediaKeyNeededEventBinding.h"
+#include "nsContentUtils.h"
+#include "jsfriendapi.h"
+#include "nsINode.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaKeyNeededEvent)
+
+NS_IMPL_ADDREF_INHERITED(MediaKeyNeededEvent, Event)
+NS_IMPL_RELEASE_INHERITED(MediaKeyNeededEvent, Event)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaKeyNeededEvent, Event)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaKeyNeededEvent, Event)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mInitData)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaKeyNeededEvent, Event)
+  tmp->mInitData = nullptr;
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaKeyNeededEvent)
+NS_INTERFACE_MAP_END_INHERITING(Event)
+
+MediaKeyNeededEvent::MediaKeyNeededEvent(EventTarget* aOwner)
+  : Event(aOwner, nullptr, nullptr)
+{
+  mozilla::HoldJSObjects(this);
+}
+
+MediaKeyNeededEvent::~MediaKeyNeededEvent()
+{
+  mInitData = nullptr;
+  mozilla::DropJSObjects(this);
+}
+
+JSObject*
+MediaKeyNeededEvent::WrapObject(JSContext* aCx)
+{
+  return MediaKeyNeededEventBinding::Wrap(aCx, this);
+}
+
+already_AddRefed<MediaKeyNeededEvent>
+MediaKeyNeededEvent::Constructor(EventTarget* aOwner,
+                                 const nsAString& aInitDataType,
+                                 const nsTArray<uint8_t>& aInitData)
+{
+  nsRefPtr<MediaKeyNeededEvent> e = new MediaKeyNeededEvent(aOwner);
+  e->InitEvent(NS_LITERAL_STRING("needkey"), false, false);
+  e->mInitDataType = aInitDataType;
+  e->mRawInitData = aInitData;
+  e->SetTrusted(true);
+  return e.forget();
+}
+
+already_AddRefed<MediaKeyNeededEvent>
+MediaKeyNeededEvent::Constructor(const GlobalObject& aGlobal,
+                                 const nsAString& aType,
+                                 const MediaKeyNeededEventInit& aEventInitDict,
+                                 ErrorResult& aRv)
+{
+  nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports());
+  nsRefPtr<MediaKeyNeededEvent> e = new MediaKeyNeededEvent(owner);
+  bool trusted = e->Init(owner);
+  e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable);
+  e->mInitDataType = aEventInitDict.mInitDataType;
+  if (aEventInitDict.mInitData.WasPassed() &&
+      !aEventInitDict.mInitData.Value().IsNull()) {
+    const auto& a = aEventInitDict.mInitData.Value().Value();
+    a.ComputeLengthAndData();
+    e->mInitData = Uint8Array::Create(aGlobal.GetContext(), owner, a.Length(), a.Data());
+    if (!e->mInitData) {
+      aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+      return nullptr;
+    }
+  }
+  e->SetTrusted(trusted);
+  return e.forget();
+}
+
+void
+MediaKeyNeededEvent::GetInitDataType(nsString& aRetVal) const
+{
+  aRetVal = mInitDataType;
+}
+
+JSObject*
+MediaKeyNeededEvent::GetInitData(JSContext* cx, ErrorResult& aRv)
+{
+  if (mRawInitData.Length()) {
+    mInitData = Uint8Array::Create(cx,
+                                   this,
+                                   mRawInitData.Length(),
+                                   mRawInitData.Elements());
+    if (!mInitData) {
+      aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+      return nullptr;
+    }
+    mRawInitData.Clear();
+  }
+  if (mInitData) {
+    JS::ExposeObjectToActiveJS(mInitData);
+  }
+  return mInitData;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/content/media/eme/MediaKeyNeededEvent.h
@@ -0,0 +1,62 @@
+/* -*- 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_MediaKeyNeededEvent_h__
+#define mozilla_dom_MediaKeyNeededEvent_h__
+
+#include "mozilla/dom/MediaKeyNeededEventBinding.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+#include "nsCOMPtr.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "js/TypeDecls.h"
+
+namespace mozilla {
+namespace dom {
+
+class MediaKeyNeededEvent MOZ_FINAL : public Event
+{
+public:
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MediaKeyNeededEvent, Event)
+  virtual ~MediaKeyNeededEvent();
+protected:
+  MediaKeyNeededEvent(EventTarget* aOwner);
+
+  nsString mInitDataType;
+  JS::Heap<JSObject*> mInitData;
+
+public:
+
+  virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE;
+
+  static already_AddRefed<MediaKeyNeededEvent>
+  Constructor(EventTarget* aOwner,
+              const nsAString& aInitDataType,
+              const nsTArray<uint8_t>& aInitData);
+
+  static already_AddRefed<MediaKeyNeededEvent>
+  Constructor(const GlobalObject& aGlobal,
+              const nsAString& aType,
+              const MediaKeyNeededEventInit& aEventInitDict,
+              ErrorResult& aRv);
+
+  void GetInitDataType(nsString& aRetVal) const;
+
+  JSObject* GetInitData(JSContext* cx, ErrorResult& aRv);
+private:
+  nsTArray<uint8_t> mRawInitData;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_MediaKeyNeededEvent_h__
new file mode 100644
--- /dev/null
+++ b/content/media/eme/MediaKeySession.cpp
@@ -0,0 +1,178 @@
+/* -*- 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/HTMLMediaElement.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/dom/MediaKeyError.h"
+#include "mozilla/dom/MediaKeyMessageEvent.h"
+#include "mozilla/dom/MediaKeyNeededEvent.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/CDMProxy.h"
+#include "mozilla/AsyncEventDispatcher.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaKeySession,
+                                   DOMEventTargetHelper,
+                                   mMediaKeyError,
+                                   mKeys,
+                                   mClosed)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaKeySession)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_ADDREF_INHERITED(MediaKeySession, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(MediaKeySession, DOMEventTargetHelper)
+
+MediaKeySession::MediaKeySession(nsPIDOMWindow* aParent,
+                                 MediaKeys* aKeys,
+                                 const nsAString& aKeySystem,
+                                 SessionType aSessionType)
+  : DOMEventTargetHelper(aParent)
+  , mKeys(aKeys)
+  , mKeySystem(aKeySystem)
+  , mSessionType(aSessionType)
+  , mIsClosed(false)
+{
+  MOZ_ASSERT(aParent);
+  mClosed = mKeys->MakePromise();
+}
+
+void MediaKeySession::Init(const nsAString& aSessionId)
+{
+  mSessionId = aSessionId;
+}
+
+MediaKeySession::~MediaKeySession()
+{
+}
+
+MediaKeyError*
+MediaKeySession::GetError() const
+{
+  return mMediaKeyError;
+}
+
+void
+MediaKeySession::GetKeySystem(nsString& aKeySystem) const
+{
+  aKeySystem = mKeySystem;
+}
+
+void
+MediaKeySession::GetSessionId(nsString& aSessionId) const
+{
+  aSessionId = mSessionId;
+}
+
+JSObject*
+MediaKeySession::WrapObject(JSContext* aCx)
+{
+  return MediaKeySessionBinding::Wrap(aCx, this);
+}
+
+double
+MediaKeySession::Expiration() const
+{
+  return JS::GenericNaN();
+}
+
+Promise*
+MediaKeySession::Closed() const
+{
+  return mClosed;
+}
+
+already_AddRefed<Promise>
+MediaKeySession::Update(const Uint8Array& aResponse)
+{
+  nsRefPtr<Promise> promise(mKeys->MakePromise());
+  aResponse.ComputeLengthAndData();
+  if (IsClosed() ||
+      !mKeys->GetCDMProxy() ||
+      !aResponse.Length()) {
+    promise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR);
+    return promise.forget();
+  }
+  mKeys->GetCDMProxy()->UpdateSession(mSessionId,
+                                      mKeys->StorePromise(promise),
+                                      aResponse);
+  return promise.forget();
+}
+
+already_AddRefed<Promise>
+MediaKeySession::Close()
+{
+  nsRefPtr<Promise> promise(mKeys->MakePromise());
+  if (IsClosed() || !mKeys->GetCDMProxy()) {
+    promise->MaybeResolve(JS::UndefinedHandleValue);
+    return promise.forget();
+  }
+  mKeys->GetCDMProxy()->CloseSession(mSessionId, mKeys->StorePromise(promise));
+
+  return promise.forget();
+}
+
+void
+MediaKeySession::OnClosed()
+{
+  if (IsClosed()) {
+    return;
+  }
+  mIsClosed = true;
+  // TODO: reset usableKeyIds
+  mKeys->OnSessionClosed(this);
+  mKeys = nullptr;
+  mClosed->MaybeResolve(JS::UndefinedHandleValue);
+}
+
+bool
+MediaKeySession::IsClosed() const
+{
+  return mIsClosed;
+}
+
+already_AddRefed<Promise>
+MediaKeySession::Remove()
+{
+  nsRefPtr<Promise> promise(mKeys->MakePromise());
+  if (mSessionType != SessionType::Persistent) {
+    promise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR);
+    // "The operation is not supported on session type sessions."
+    return promise.forget();
+  }
+  if (IsClosed() || !mKeys->GetCDMProxy()) {
+    promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+    // "The session is closed."
+    return promise.forget();
+  }
+  mKeys->GetCDMProxy()->RemoveSession(mSessionId, mKeys->StorePromise(promise));
+  return promise.forget();
+}
+
+void
+MediaKeySession::DispatchKeyMessage(const nsTArray<uint8_t>& aMessage,
+                                    const nsString& aURL)
+{
+  nsRefPtr<MediaKeyMessageEvent> event(
+    MediaKeyMessageEvent::Constructor(this, aURL, aMessage));
+  nsRefPtr<AsyncEventDispatcher> asyncDispatcher =
+    new AsyncEventDispatcher(this, event);
+  asyncDispatcher->PostDOMEvent();
+}
+
+void
+MediaKeySession::DispatchKeyError(uint32_t aSystemCode)
+{
+  RefPtr<MediaKeyError> event(new MediaKeyError(this, aSystemCode));
+  nsRefPtr<AsyncEventDispatcher> asyncDispatcher =
+    new AsyncEventDispatcher(this, event);
+  asyncDispatcher->PostDOMEvent();
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/content/media/eme/MediaKeySession.h
@@ -0,0 +1,93 @@
+/* -*- 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_MediaKeySession_h
+#define mozilla_dom_MediaKeySession_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "nsCOMPtr.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/dom/Date.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/MediaKeySessionBinding.h"
+#include "mozilla/dom/MediaKeysBinding.h"
+
+struct JSContext;
+
+namespace mozilla {
+
+class CDMProxy;
+
+namespace dom {
+
+class MediaKeyError;
+
+class MediaKeySession MOZ_FINAL : public DOMEventTargetHelper
+{
+public:
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaKeySession,
+                                           DOMEventTargetHelper)
+public:
+  MediaKeySession(nsPIDOMWindow* aParent,
+                  MediaKeys* aKeys,
+                  const nsAString& aKeySystem,
+                  SessionType aSessionType);
+
+  void Init(const nsAString& aSessionId);
+
+  ~MediaKeySession();
+
+  virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE;
+
+  // Mark this as resultNotAddRefed to return raw pointers
+  MediaKeyError* GetError() const;
+
+  void GetKeySystem(nsString& aRetval) const;
+
+  void GetSessionId(nsString& aRetval) const;
+
+  // Number of ms since epoch at which expiration occurs, or NaN if unknown.
+  // TODO: The type of this attribute is still under contention.
+  // https://www.w3.org/Bugs/Public/show_bug.cgi?id=25902
+  double Expiration() const;
+
+  Promise* Closed() const;
+
+  already_AddRefed<Promise> Update(const Uint8Array& response);
+
+  already_AddRefed<Promise> Close();
+
+  already_AddRefed<Promise> Remove();
+
+  void DispatchKeyMessage(const nsTArray<uint8_t>& aMessage,
+                          const nsString& aURL);
+
+  void DispatchKeyError(uint32_t system_code);
+
+  void OnClosed();
+
+  bool IsClosed() const;
+
+private:
+  nsRefPtr<Promise> mClosed;
+
+  nsRefPtr<MediaKeyError> mMediaKeyError;
+  nsRefPtr<MediaKeys> mKeys;
+  const nsString mKeySystem;
+  nsString mSessionId;
+  const SessionType mSessionType;
+  bool mIsClosed;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/content/media/eme/MediaKeys.cpp
@@ -0,0 +1,291 @@
+/* -*- 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/HTMLMediaElement.h"
+#include "mozilla/dom/MediaKeys.h"
+#include "mozilla/dom/MediaKeysBinding.h"
+#include "mozilla/dom/MediaKeyMessageEvent.h"
+#include "mozilla/dom/MediaKeyError.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/CDMProxy.h"
+#include "nsContentUtils.h"
+#include "EMELog.h"
+
+namespace mozilla {
+
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaKeys,
+                                      mParent,
+                                      mKeySessions,
+                                      mPromises,
+                                      mPendingSessions);
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeys)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeys)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeys)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+MediaKeys::MediaKeys(nsPIDOMWindow* aParent, const nsAString& aKeySystem)
+  : mParent(aParent),
+    mKeySystem(aKeySystem)
+{
+  SetIsDOMBinding();
+}
+
+MediaKeys::~MediaKeys()
+{
+  if (mProxy) {
+    mProxy->Shutdown();
+    mProxy = nullptr;
+  }
+}
+
+nsPIDOMWindow*
+MediaKeys::GetParentObject() const
+{
+  return mParent;
+}
+
+JSObject*
+MediaKeys::WrapObject(JSContext* aCx)
+{
+  return MediaKeysBinding::Wrap(aCx, this);
+}
+
+void
+MediaKeys::GetKeySystem(nsString& retval) const
+{
+  retval = mKeySystem;
+}
+
+already_AddRefed<Promise>
+MediaKeys::SetServerCertificate(const Uint8Array& aCert)
+{
+  aCert.ComputeLengthAndData();
+  nsRefPtr<Promise> promise(MakePromise());
+  mProxy->SetServerCertificate(StorePromise(promise), aCert);
+  return promise.forget();
+}
+
+/* static */
+IsTypeSupportedResult
+MediaKeys::IsTypeSupported(const GlobalObject& aGlobal,
+                           const nsAString& aKeySystem,
+                           const Optional<nsAString>& aInitDataType,
+                           const Optional<nsAString>& aContentType,
+                           const Optional<nsAString>& aCapability)
+{
+  // TODO: Query list of known CDMs and their supported content types.
+  // TODO: Should really get spec changed to this is async, so we can wait
+  //       for user to consent to running plugin.
+  return IsTypeSupportedResult::Maybe;
+}
+
+already_AddRefed<Promise>
+MediaKeys::MakePromise()
+{
+  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject());
+  if (!global) {
+    NS_WARNING("Passed non-global to MediaKeys ctor!");
+    return nullptr;
+  }
+  nsRefPtr<Promise> promise = new Promise(global);
+  return promise.forget();
+}
+
+PromiseId
+MediaKeys::StorePromise(Promise* aPromise)
+{
+  static uint32_t sEMEPromiseCount = 1;
+  MOZ_ASSERT(aPromise);
+  uint32_t id = sEMEPromiseCount++;
+  mPromises.Put(id, aPromise);
+  return id;
+}
+
+already_AddRefed<Promise>
+MediaKeys::RetrievePromise(PromiseId aId)
+{
+  nsRefPtr<Promise> promise;
+  mPromises.Remove(aId, getter_AddRefs(promise));
+  return promise.forget();
+}
+
+void
+MediaKeys::RejectPromise(PromiseId aId, nsresult aExceptionCode)
+{
+  nsRefPtr<Promise> promise(RetrievePromise(aId));
+  if (!promise) {
+    NS_WARNING("MediaKeys tried to reject a non-existent promise");
+    return;
+  }
+  if (mPendingSessions.Contains(aId)) {
+    // This promise could be a createSession or loadSession promise,
+    // so we might have a pending session waiting to be resolved into
+    // the promise on success. We've been directed to reject to promise,
+    // so we can throw away the corresponding session object.
+    mPendingSessions.Remove(aId);
+  }
+
+  MOZ_ASSERT(NS_FAILED(aExceptionCode));
+  promise->MaybeReject(aExceptionCode);
+}
+
+void
+MediaKeys::ResolvePromise(PromiseId aId)
+{
+  nsRefPtr<Promise> promise(RetrievePromise(aId));
+  if (!promise) {
+    NS_WARNING("MediaKeys tried to resolve a non-existent promise");
+    return;
+  }
+  // We should not resolve CreateSession or LoadSession calls via this path,
+  // OnSessionActivated() should be called instead.
+  MOZ_ASSERT(!mPendingSessions.Contains(aId));
+  promise->MaybeResolve(JS::UndefinedHandleValue);
+}
+
+/* static */
+already_AddRefed<Promise>
+MediaKeys::Create(const GlobalObject& aGlobal,
+                  const nsAString& aKeySystem,
+                  ErrorResult& aRv)
+{
+  // CDMProxy keeps MediaKeys alive until it resolves the promise and thus
+  // returns the MediaKeys object to JS.
+  nsCOMPtr<nsPIDOMWindow> window = do_QueryInterface(aGlobal.GetAsSupports());
+  if (!window) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  nsRefPtr<MediaKeys> keys = new MediaKeys(window, aKeySystem);
+  nsRefPtr<Promise> promise(keys->MakePromise());
+  if (!promise) {
+    aRv.Throw(NS_ERROR_UNEXPECTED);
+    return nullptr;
+  }
+
+  if (!aKeySystem.EqualsASCII("org.w3.clearkey")) {
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return nullptr;
+  }
+
+  keys->mProxy = new CDMProxy(keys, aKeySystem);
+  keys->mProxy->Init(keys->StorePromise(promise));
+
+  return promise.forget();
+}
+
+void
+MediaKeys::OnCDMCreated(PromiseId aId)
+{
+  nsRefPtr<Promise> promise(RetrievePromise(aId));
+  if (!promise) {
+    NS_WARNING("MediaKeys tried to resolve a non-existent promise");
+    return;
+  }
+  nsRefPtr<MediaKeys> keys(this);
+  promise->MaybeResolve(keys);
+}
+
+already_AddRefed<Promise>
+MediaKeys::LoadSession(const nsAString& aSessionId)
+{
+  nsRefPtr<Promise> promise(MakePromise());
+
+  if (aSessionId.IsEmpty()) {
+    promise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR);
+    // "The sessionId parameter is empty."
+    return promise.forget();
+  }
+
+  // TODO: The spec doesn't specify what to do in this case...
+  if (mKeySessions.Contains(aSessionId)) {
+    promise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR);
+    return promise.forget();
+  }
+
+  // Create session.
+  nsRefPtr<MediaKeySession> session(
+    new MediaKeySession(GetParentObject(), this, mKeySystem, SessionType::Persistent));
+
+  // Proxy owns session object until resolving promise.
+  mProxy->LoadSession(StorePromise(promise),
+                      aSessionId);
+
+  return promise.forget();
+}
+
+already_AddRefed<Promise>
+MediaKeys::CreateSession(const nsAString& initDataType,
+                         const Uint8Array& aInitData,
+                         SessionType aSessionType)
+{
+  aInitData.ComputeLengthAndData();
+  nsRefPtr<Promise> promise(MakePromise());
+  nsRefPtr<MediaKeySession> session = new MediaKeySession(GetParentObject(),
+                                                          this,
+                                                          mKeySystem,
+                                                          aSessionType);
+  auto pid = StorePromise(promise);
+  // Hang onto session until the CDM has finished setting it up.
+  mPendingSessions.Put(pid, session);
+  mProxy->CreateSession(aSessionType,
+                        pid,
+                        initDataType,
+                        aInitData);
+
+  return promise.forget();
+}
+
+void
+MediaKeys::OnSessionActivated(PromiseId aId, const nsAString& aSessionId)
+{
+  nsRefPtr<Promise> promise(RetrievePromise(aId));
+  if (!promise) {
+    NS_WARNING("MediaKeys tried to resolve a non-existent promise");
+    return;
+  }
+  MOZ_ASSERT(mPendingSessions.Contains(aId));
+
+  nsRefPtr<MediaKeySession> session;
+  if (!mPendingSessions.Get(aId, getter_AddRefs(session)) || !session) {
+    NS_WARNING("Received activation for non-existent session!");
+    promise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR);
+    return;
+  }
+
+  // Session has completed creation/loading, remove it from mPendingSessions,
+  // and resolve the promise with it. We store it in mKeySessions, so we can
+  // find it again if we need to send messages to it etc.
+  mPendingSessions.Remove(aId);
+  session->Init(aSessionId);
+  mKeySessions.Put(aSessionId, session);
+  promise->MaybeResolve(session);
+}
+
+void
+MediaKeys::OnSessionClosed(MediaKeySession* aSession)
+{
+  nsAutoString id;
+  aSession->GetSessionId(id);
+  mKeySessions.Remove(id);
+}
+
+already_AddRefed<MediaKeySession>
+MediaKeys::GetSession(const nsAString& aSessionId)
+{
+  nsRefPtr<MediaKeySession> session;
+  mKeySessions.Get(aSessionId, getter_AddRefs(session));
+  return session.forget();
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/content/media/eme/MediaKeys.h
@@ -0,0 +1,120 @@
+/* -*- 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_mediakeys_h__
+#define mozilla_dom_mediakeys_h__
+
+#include "nsIDOMMediaError.h"
+#include "nsWrapperCache.h"
+#include "nsISupports.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsRefPtrHashtable.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/MediaKeysBinding.h"
+
+namespace mozilla {
+
+class CDMProxy;
+
+namespace dom {
+
+class MediaKeySession;
+
+typedef nsRefPtrHashtable<nsStringHashKey, MediaKeySession> KeySessionHashMap;
+typedef nsRefPtrHashtable<nsUint32HashKey, dom::Promise> PromiseHashMap;
+typedef nsRefPtrHashtable<nsUint32HashKey, MediaKeySession> PendingKeySessionsHashMap;
+typedef uint32_t PromiseId;
+
+// This class is used on the main thread only.
+// Note: it's addref/release is not (and can't be) thread safe!
+class MediaKeys MOZ_FINAL : public nsISupports,
+                            public nsWrapperCache
+{
+public:
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MediaKeys)
+
+  MediaKeys(nsPIDOMWindow* aParentWindow, const nsAString& aKeySystem);
+
+  ~MediaKeys();
+
+  nsPIDOMWindow* GetParentObject() const;
+
+  virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE;
+
+  // Javascript: readonly attribute DOMString keySystem;
+  void GetKeySystem(nsString& retval) const;
+
+  // JavaScript: MediaKeys.createSession()
+  already_AddRefed<Promise> CreateSession(const nsAString& aInitDataType,
+                                          const Uint8Array& aInitData,
+                                          SessionType aSessionType);
+
+  // JavaScript: MediaKeys.loadSession()
+  already_AddRefed<Promise> LoadSession(const nsAString& aSessionId);
+
+  // JavaScript: MediaKeys.SetServerCertificate()
+  already_AddRefed<Promise> SetServerCertificate(const Uint8Array& aServerCertificate);
+
+  // JavaScript: MediaKeys.create()
+  static
+  already_AddRefed<Promise> Create(const GlobalObject& aGlobal,
+                                   const nsAString& aKeySystem,
+                                   ErrorResult& aRv);
+
+  // JavaScript: MediaKeys.IsTypeSupported()
+  static IsTypeSupportedResult IsTypeSupported(const GlobalObject& aGlobal,
+                                               const nsAString& aKeySystem,
+                                               const Optional<nsAString>& aInitDataType,
+                                               const Optional<nsAString>& aContentType,
+                                               const Optional<nsAString>& aCapability);
+
+  already_AddRefed<MediaKeySession> GetSession(const nsAString& aSessionId);
+
+  // Called once a Create() operation succeeds.
+  void OnCDMCreated(PromiseId aId);
+  // Called once a CreateSession or LoadSession succeeds.
+  void OnSessionActivated(PromiseId aId, const nsAString& aSessionId);
+  // Called once a session has closed.
+  void OnSessionClosed(MediaKeySession* aSession);
+
+  CDMProxy* GetCDMProxy() { return mProxy; }
+
+  // Makes a new promise, or nullptr on failure.
+  already_AddRefed<Promise> MakePromise();
+  // Stores promise in mPromises, returning an ID that can be used to retrieve
+  // it later. The ID is passed to the CDM, so that it can signal specific
+  // promises to be resolved.
+  PromiseId StorePromise(Promise* aPromise);
+
+  // Reject promise with DOMException corresponding to aExceptionCode.
+  void RejectPromise(PromiseId aId, nsresult aExceptionCode);
+  // Resolves promise with "undefined".
+  void ResolvePromise(PromiseId aId);
+
+private:
+
+  // Removes promise from mPromises, and returns it.
+  already_AddRefed<Promise> RetrievePromise(PromiseId aId);
+
+  // Owning ref to proxy. The proxy has a weak reference back to the MediaKeys,
+  // and the MediaKeys destructor clears the proxy's reference to the MediaKeys.
+  nsRefPtr<CDMProxy> mProxy;
+
+  nsCOMPtr<nsPIDOMWindow> mParent;
+  nsString mKeySystem;
+  KeySessionHashMap mKeySessions;
+  PromiseHashMap mPromises;
+  PendingKeySessionsHashMap mPendingSessions;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_mediakeys_h__
new file mode 100644
--- /dev/null
+++ b/content/media/eme/moz.build
@@ -0,0 +1,31 @@
+# -*- 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/.
+
+EXPORTS.mozilla.dom += [
+    'MediaKeyError.h',
+    'MediaKeyMessageEvent.h',
+    'MediaKeyNeededEvent.h',
+    'MediaKeys.h',
+    'MediaKeySession.h',
+]
+
+EXPORTS.mozilla += [
+    'CDMProxy.h',
+]
+
+UNIFIED_SOURCES += [
+    'CDMProxy.cpp',
+    'EMELog.cpp',
+    'MediaKeyError.cpp',
+    'MediaKeyMessageEvent.cpp',
+    'MediaKeyNeededEvent.cpp',
+    'MediaKeys.cpp',
+    'MediaKeySession.cpp',
+]
+
+FINAL_LIBRARY = 'gklayout'
+
+FAIL_ON_WARNINGS = True
--- a/content/media/moz.build
+++ b/content/media/moz.build
@@ -45,16 +45,18 @@ if CONFIG['MOZ_APPLEMEDIA']:
 PARALLEL_DIRS += ['webrtc']
 
 if CONFIG['MOZ_OMX_DECODER']:
     PARALLEL_DIRS += ['omx']
     PARALLEL_DIRS += ['omx/mediaresourcemanager']
 
 PARALLEL_DIRS += ['webspeech']
 
+PARALLEL_DIRS += ['eme']
+
 TEST_DIRS += [
     'test',
     'gtest',
 ]
 
 EXPORTS += [
     'AbstractMediaDecoder.h',
     'AudioChannelFormat.h',
--- a/dom/events/EventNameList.h
+++ b/dom/events/EventNameList.h
@@ -288,17 +288,16 @@ EVENT(mozfullscreenerror,
 EVENT(mozpointerlockchange,
       NS_POINTERLOCKCHANGE,
       EventNameType_HTML,
       NS_EVENT)
 EVENT(mozpointerlockerror,
       NS_POINTERLOCKERROR,
       EventNameType_HTML,
       NS_EVENT)
-
 EVENT(pointerdown,
       NS_POINTER_DOWN,
       EventNameType_All,
       NS_POINTER_EVENT)
 EVENT(pointermove,
       NS_POINTER_MOVE,
       EventNameType_All,
       NS_POINTER_EVENT)
--- a/dom/webidl/HTMLMediaElement.webidl
+++ b/dom/webidl/HTMLMediaElement.webidl
@@ -125,8 +125,30 @@ partial interface HTMLMediaElement {
   [SetterThrows]
   attribute AudioChannel mozAudioChannelType;
 
   // In addition the media element has this new events:
   // * onmozinterruptbegin - called when the media element is interrupted
   //   because of the audiochannel manager.
   // * onmozinterruptend - called when the interruption is concluded
 };
+
+enum MediaWaitingFor {
+  "none",
+  "data",
+  "key"
+};
+
+// Encrypted Media Extensions
+partial interface HTMLMediaElement {
+  [Pref="media.eme.enabled"]
+  readonly attribute MediaKeys? mediaKeys;
+  
+  // Promise<any>
+  [Pref="media.eme.enabled", Throws, NewObject]
+  Promise setMediaKeys(MediaKeys? mediaKeys);
+  
+  [Pref="media.eme.enabled"]
+  attribute EventHandler onneedkey;
+
+  [Pref="media.eme.enabled"]
+  readonly attribute MediaWaitingFor waitingFor;
+};
--- a/dom/webidl/HTMLSourceElement.webidl
+++ b/dom/webidl/HTMLSourceElement.webidl
@@ -14,8 +14,14 @@
 interface HTMLSourceElement : HTMLElement {
            [SetterThrows]
            attribute DOMString src;
            [SetterThrows]
            attribute DOMString type;
            [SetterThrows]
            attribute DOMString media;
 };
+
+// Encrypted Media Extensions
+partial interface HTMLSourceElement {
+  [Pref="media.eme.enabled"]
+  attribute DOMString keySystem;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/MediaKeyError.webidl
@@ -0,0 +1,19 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * The origin of this IDL file is
+ * https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html
+ *
+ * Copyright © 2014 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved.
+ * W3C liability, trademark and document use rules apply.
+ */
+
+// According to the spec, "The future of error events and MediaKeyError
+// is uncertain."
+// https://www.w3.org/Bugs/Public/show_bug.cgi?id=21798
+[Pref="media.eme.enabled"]
+interface MediaKeyError : Event {
+  readonly attribute unsigned long systemCode;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/MediaKeyMessageEvent.webidl
@@ -0,0 +1,23 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * The origin of this IDL file is
+ * https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html
+ *
+ * Copyright © 2014 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved.
+ * W3C liability, trademark and document use rules apply.
+ */
+
+[Pref="media.eme.enabled", Constructor(DOMString type, optional MediaKeyMessageEventInit eventInitDict)]
+interface MediaKeyMessageEvent : Event {
+  [Throws]
+  readonly attribute Uint8Array message;
+  readonly attribute DOMString? destinationURL;
+};
+
+dictionary MediaKeyMessageEventInit : EventInit {
+  Uint8Array message;
+  DOMString? destinationURL = "";
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/MediaKeyNeededEvent.webidl
@@ -0,0 +1,23 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * The origin of this IDL file is
+ * https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html
+ *
+ * Copyright © 2014 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved.
+ * W3C liability, trademark and document use rules apply.
+ */
+
+[Pref="media.eme.enabled", Constructor(DOMString type, optional MediaKeyNeededEventInit eventInitDict)]
+interface MediaKeyNeededEvent : Event {
+  readonly attribute DOMString initDataType;
+  [Throws]
+  readonly attribute Uint8Array? initData;
+};
+
+dictionary MediaKeyNeededEventInit : EventInit {
+  DOMString initDataType = "";
+  Uint8Array? initData;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/MediaKeySession.webidl
@@ -0,0 +1,43 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * The origin of this IDL file is
+ * https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html
+ *
+ * Copyright © 2014 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved.
+ * W3C liability, trademark and document use rules apply.
+ */
+
+[Pref="media.eme.enabled"]
+interface MediaKeySession : EventTarget {
+  // error state
+  readonly attribute MediaKeyError? error;
+
+  // session properties
+  readonly attribute DOMString keySystem;
+  readonly attribute DOMString sessionId;
+
+  // Invalid WebIDL, doesn't work.
+  // https://www.w3.org/Bugs/Public/show_bug.cgi?id=25594
+  // readonly attribute Array<Uint8Array> usableKeyIds;
+
+  readonly attribute unrestricted double expiration;
+
+  // Promise<any>
+  readonly attribute Promise closed;
+
+  // session operations
+  //Promise<any>
+  [NewObject]
+  Promise update(Uint8Array response);
+
+  // Promise<any>
+  [NewObject]
+  Promise close();
+
+  // Promise<any>
+  [NewObject]
+  Promise remove();
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/MediaKeys.webidl
@@ -0,0 +1,37 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * The origin of this IDL file is
+ * https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html
+ *
+ * Copyright © 2014 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved.
+ * W3C liability, trademark and document use rules apply.
+ */
+
+enum IsTypeSupportedResult { "" /* empty string */, "maybe", "probably" };
+enum SessionType { "temporary", "persistent" };
+
+[Pref="media.eme.enabled"]
+interface MediaKeys {
+  readonly attribute DOMString keySystem;
+
+  // Promise<MediaKeySession>
+  [NewObject]
+  Promise createSession(DOMString initDataType, Uint8Array initData, optional SessionType sessionType = "temporary");
+
+  // Promise<MediaKeySession>
+  [NewObject]
+  Promise loadSession(DOMString sessionId);
+
+  // Promise<any>
+  [NewObject]
+  Promise setServerCertificate(Uint8Array serverCertificate);
+
+  // Promise<MediaKeys>
+  [Throws,NewObject]
+  static Promise create(DOMString keySystem);
+  static IsTypeSupportedResult isTypeSupported(DOMString keySystem, optional DOMString initDataType, optional DOMString contentType, optional DOMString capability);
+
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -226,16 +226,21 @@ WEBIDL_FILES = [
     'KeyEvent.webidl',
     'LegacyQueryInterface.webidl',
     'LinkStyle.webidl',
     'LocalMediaStream.webidl',
     'Location.webidl',
     'LockedFile.webidl',
     'MediaElementAudioSourceNode.webidl',
     'MediaError.webidl',
+    'MediaKeyError.webidl',
+    'MediaKeyMessageEvent.webidl',
+    'MediaKeyNeededEvent.webidl',
+    'MediaKeys.webidl',
+    'MediaKeySession.webidl',
     'MediaList.webidl',
     'MediaQueryList.webidl',
     'MediaRecorder.webidl',
     'MediaSource.webidl',
     'MediaStream.webidl',
     'MediaStreamAudioDestinationNode.webidl',
     'MediaStreamAudioSourceNode.webidl',
     'MediaStreamTrack.webidl',
--- a/dom/workers/WorkerFeature.h
+++ b/dom/workers/WorkerFeature.h
@@ -23,16 +23,22 @@ BEGIN_WORKERS_NAMESPACE
  * +-------------+-------------+-----------------+----------------+
  * | Terminating |     yes     |       yes       |   no timeout   |
  * +-------------+-------------+-----------------+----------------+
  * |  Canceling  |     yes     |       yes       | short duration |
  * +-------------+-------------+-----------------+----------------+
  * |   Killing   |     yes     |       yes       |   doesn't run  |
  * +-------------+-------------+-----------------+----------------+
  */
+
+#ifdef Status
+/* Xlib headers insist on this for some reason... Nuke it because
+   it'll override our member name */
+#undef Status
+#endif
 enum Status
 {
   // Not yet scheduled.
   Pending = 0,
 
   // This status means that the close handler has not yet been scheduled.
   Running,
 
--- a/parser/html/nsHtml5AtomList.h
+++ b/parser/html/nsHtml5AtomList.h
@@ -163,16 +163,17 @@ HTML5_ATOM(fence, "fence")
 HTML5_ATOM(frame, "frame")
 HTML5_ATOM(ismap, "ismap")
 HTML5_ATOM(onend, "onend")
 HTML5_ATOM(index, "index")
 HTML5_ATOM(order, "order")
 HTML5_ATOM(other, "other")
 HTML5_ATOM(oncut, "oncut")
 HTML5_ATOM(nargs, "nargs")
+HTML5_ATOM(keysystem, "keysystem")
 HTML5_ATOM(media, "media")
 HTML5_ATOM(label, "label")
 HTML5_ATOM(local, "local")
 HTML5_ATOM(width, "width")
 HTML5_ATOM(vlink, "vlink")
 HTML5_ATOM(value, "value")
 HTML5_ATOM(slope, "slope")
 HTML5_ATOM(shape, "shape")
--- a/widget/BasicEvents.h
+++ b/widget/BasicEvents.h
@@ -317,16 +317,17 @@ enum nsEventStructType
 #define NS_CANPLAYTHROUGH      (NS_MEDIA_EVENT_START+12)
 #define NS_SEEKING             (NS_MEDIA_EVENT_START+13)
 #define NS_SEEKED              (NS_MEDIA_EVENT_START+14)
 #define NS_TIMEUPDATE          (NS_MEDIA_EVENT_START+15)
 #define NS_ENDED               (NS_MEDIA_EVENT_START+16)
 #define NS_RATECHANGE          (NS_MEDIA_EVENT_START+17)
 #define NS_DURATIONCHANGE      (NS_MEDIA_EVENT_START+18)
 #define NS_VOLUMECHANGE        (NS_MEDIA_EVENT_START+19)
+#define NS_NEED_KEY            (NS_MEDIA_EVENT_START+20)
 
 // paint notification events
 #define NS_NOTIFYPAINT_START    3400
 #define NS_AFTERPAINT           (NS_NOTIFYPAINT_START)
 
 // Simple gesture events
 #define NS_SIMPLE_GESTURE_EVENT_START    3500
 #define NS_SIMPLE_GESTURE_SWIPE_START    (NS_SIMPLE_GESTURE_EVENT_START)