Bug 1351953 - Pre-allocate shmems for the CDM process to use for storing decrypted and audio samples. r=gerald draft
authorChris Pearce <cpearce@mozilla.com>
Tue, 28 Mar 2017 18:59:11 +1300
changeset 556491 a0cb126e72bfb2905bcdf02e864dc654e8340410
parent 556490 44dfbc465db14bb689a653e6c0b3cbc626c0a0d1
child 556492 0ca1caf0843ce51f26d3a93ea1037fe5baa0ade6
child 556493 dac1ea23c64cd68354cd4532203acd36cce9f390
push id52568
push userbmo:cpearce@mozilla.com
push dateWed, 05 Apr 2017 23:29:31 +0000
reviewersgerald
bugs1351953
milestone55.0a1
Bug 1351953 - Pre-allocate shmems for the CDM process to use for storing decrypted and audio samples. r=gerald Makes transfer of samples between the content and CDM processes use shmems. The Chromium CDM API requires us to implement a synchronous interface to supply buffers to the CDM for it to write decrypted samples into. We want our buffers to be backed by shmems, in order to reduce the overhead of transferring decoded frames. However due to sandboxing restrictions, the CDM process cannot allocate shmems itself. We don't want to be doing synchronous IPC to request shmems from the content process, nor do we want to have to do intr IPC or make async IPC conform to the sync allocation interface. So instead we have the content process pre-allocate a set of shmems and give them to the CDM process in advance of them being needed. When the CDM needs to allocate a buffer for storing a decrypted sample, the CDM host gives it one of these shmems' buffers. When this is sent back to the content process, we copy the result out (uploading to a GPU surface for video frames), and send the shmem back to the CDM process so it can reuse it. We predict the size of buffer the CDM will allocate, and prepopulate the CDM's list of shmems with shmems of at least that size, plus a bit of padding for safety. We pad frames out to be the next multiple of 16, as we've seen some decoders do that. Normally the CDM won't allocate more than one buffer at once, but we've seen cases where it allocates two buffers, returns one and holds onto the other. So the minimum number of shmems we give to the CDM must be at least two, and the default is three for safety. MozReview-Commit-ID: 5FaWAst3aeh
dom/media/MediaPrefs.h
dom/media/gmp/ChromiumCDMChild.cpp
dom/media/gmp/ChromiumCDMChild.h
dom/media/gmp/ChromiumCDMParent.cpp
dom/media/gmp/ChromiumCDMParent.h
dom/media/gmp/GMPTypes.ipdlh
dom/media/gmp/GMPUtils.cpp
dom/media/gmp/GMPUtils.h
dom/media/gmp/PChromiumCDM.ipdl
modules/libpref/init/all.js
--- a/dom/media/MediaPrefs.h
+++ b/dom/media/MediaPrefs.h
@@ -139,16 +139,20 @@ private:
   DECL_MEDIA_PREF("media.decoder.fuzzing.dont-delay-inputexhausted", PDMFuzzingDelayInputExhausted, bool, true);
   DECL_MEDIA_PREF("media.decoder.recycle.enabled",            MediaDecoderCheckRecycling, bool, false);
   DECL_MEDIA_PREF("media.gmp.decoder.enabled",                PDMGMPEnabled, bool, true);
   DECL_MEDIA_PREF("media.gmp.decoder.aac",                    GMPAACPreferred, uint32_t, 0);
   DECL_MEDIA_PREF("media.gmp.decoder.h264",                   GMPH264Preferred, uint32_t, 0);
   DECL_MEDIA_PREF("media.eme.audio.blank",                    EMEBlankAudio, bool, false);
   DECL_MEDIA_PREF("media.eme.video.blank",                    EMEBlankVideo, bool, false);
   DECL_MEDIA_PREF("media.eme.chromium-api.enabled",           EMEChromiumAPIEnabled, bool, false);
+  DECL_MEDIA_PREF("media.eme.chromium-api.video-shmems",
+                  EMEChromiumAPIVideoShmemCount,
+                  uint32_t,
+                  3);
 
   // MediaDecoderStateMachine
   DECL_MEDIA_PREF("media.suspend-bkgnd-video.enabled",        MDSMSuspendBackgroundVideoEnabled, bool, false);
   DECL_MEDIA_PREF("media.suspend-bkgnd-video.delay-ms",       MDSMSuspendBackgroundVideoDelay, AtomicUint32, SUSPEND_BACKGROUND_VIDEO_DELAY_MS);
   DECL_MEDIA_PREF("media.dormant-on-pause-timeout-ms",        DormantOnPauseTimeout, int32_t, 5000);
 
   // WebSpeech
   DECL_MEDIA_PREF("media.webspeech.synth.force_global_queue", WebSpeechForceGlobal, bool, false);
--- a/dom/media/gmp/ChromiumCDMChild.cpp
+++ b/dom/media/gmp/ChromiumCDMChild.cpp
@@ -10,24 +10,26 @@
 #include "WidevineVideoFrame.h"
 #include "GMPLog.h"
 #include "GMPPlatform.h"
 #include "mozilla/Unused.h"
 #include "nsPrintfCString.h"
 #include "base/time.h"
 #include "GMPUtils.h"
 #include "mozilla/ScopeExit.h"
+#include "mozilla/SizePrintfMacros.h"
 
 namespace mozilla {
 namespace gmp {
 
 ChromiumCDMChild::ChromiumCDMChild(GMPContentChild* aPlugin)
   : mPlugin(aPlugin)
 {
   MOZ_ASSERT(IsOnMessageLoopThread());
+  GMP_LOG("ChromiumCDMChild:: ctor this=%p", this);
 }
 
 void
 ChromiumCDMChild::Init(cdm::ContentDecryptionModule_8* aCDM)
 {
   MOZ_ASSERT(IsOnMessageLoopThread());
   mCDM = aCDM;
   MOZ_ASSERT(mCDM);
@@ -38,22 +40,121 @@ ChromiumCDMChild::TimerExpired(void* aCo
 {
   MOZ_ASSERT(IsOnMessageLoopThread());
   GMP_LOG("ChromiumCDMChild::TimerExpired(context=0x%p)", aContext);
   if (mCDM) {
     mCDM->TimerExpired(aContext);
   }
 }
 
+class CDMShmemBuffer : public cdm::Buffer
+{
+public:
+  CDMShmemBuffer(ChromiumCDMChild* aProtocol, ipc::Shmem aShmem)
+    : mProtocol(aProtocol)
+    , mSize(aShmem.Size<uint8_t>())
+    , mShmem(aShmem)
+  {
+    CDM_LOG("CDMShmemBuffer(size=%" PRIu32 ") created", Size());
+    // Note: Chrome initializes the size of a buffer to it capacity. We do the same.
+  }
+
+  ~CDMShmemBuffer() override
+  {
+    CDM_LOG("CDMShmemBuffer(size=%" PRIu32 ") destructed writable=%d",
+            Size(),
+            mShmem.IsWritable());
+    if (mShmem.IsWritable()) {
+      // The shmem wasn't extracted to send its data back up to the parent process,
+      // so we can reuse the shmem.
+      mProtocol->GiveBuffer(Move(mShmem));
+    }
+  }
+
+  void Destroy() override
+  {
+    CDM_LOG("CDMShmemBuffer::Destroy(size=%" PRIu32 ")", Size());
+    delete this;
+  }
+  uint32_t Capacity() const override { return mShmem.Size<uint8_t>(); }
+
+  uint8_t* Data() override { return mShmem.get<uint8_t>(); }
+
+  void SetSize(uint32_t aSize) override
+  {
+    MOZ_ASSERT(aSize <= Capacity());
+    // Note: We can't use the shmem's size member after ExtractShmem(),
+    // has been called, so we track the size exlicitly so that we can use
+    // Size() in logging after we've called ExtractShmem().
+    CDM_LOG("CDMShmemBuffer::SetSize(size=%" PRIu32 ")", Size());
+    mSize = aSize;
+  }
+
+  uint32_t Size() const override { return mSize; }
+
+  ipc::Shmem ExtractShmem()
+  {
+    ipc::Shmem shmem = mShmem;
+    mShmem = ipc::Shmem();
+    return shmem;
+  }
+
+private:
+  RefPtr<ChromiumCDMChild> mProtocol;
+  uint32_t mSize;
+  mozilla::ipc::Shmem mShmem;
+  CDMShmemBuffer(const CDMShmemBuffer&);
+  void operator=(const CDMShmemBuffer&);
+};
+
+static nsCString
+ToString(const nsTArray<ipc::Shmem>& aBuffers)
+{
+  nsCString s;
+  for (const ipc::Shmem& shmem : aBuffers) {
+    if (!s.IsEmpty()) {
+      s.AppendLiteral(",");
+    }
+    s.AppendInt(static_cast<uint32_t>(shmem.Size<uint8_t>()));
+  }
+  return s;
+}
+
 cdm::Buffer*
 ChromiumCDMChild::Allocate(uint32_t aCapacity)
 {
+  GMP_LOG("ChromiumCDMChild::Allocate(capacity=%" PRIu32 ") bufferSizes={%s}",
+          aCapacity,
+          ToString(mBuffers).get());
   MOZ_ASSERT(IsOnMessageLoopThread());
-  GMP_LOG("ChromiumCDMChild::Allocate(capacity=%" PRIu32 ")", aCapacity);
-  return new WidevineBuffer(aCapacity);
+
+  // Find the shmem with the least amount of wasted space if we were to
+  // select it for this sized allocation. We need to do this because shmems
+  // for decrypted audio as well as video frames are both stored in this
+  // list, and we don't want to use the video frame shmems for audio samples.
+  const size_t invalid = std::numeric_limits<size_t>::max();
+  size_t best = invalid;
+  auto wastedSpace = [this, aCapacity](size_t index) {
+    return mBuffers[index].Size<uint8_t>() - aCapacity;
+  };
+  for (size_t i = 0; i < mBuffers.Length(); i++) {
+    if (mBuffers[i].Size<uint8_t>() >= aCapacity &&
+        (best == invalid || wastedSpace(i) < wastedSpace(best))) {
+      best = i;
+    }
+  }
+  if (best == invalid) {
+    // The parent process should have bestowed upon us a shmem of appropriate
+    // size, but did not!
+    MOZ_ASSERT(false);
+    return nullptr;
+  }
+  ipc::Shmem shmem = mBuffers[best];
+  mBuffers.RemoveElementAt(best);
+  return new CDMShmemBuffer(this, shmem);
 }
 
 void
 ChromiumCDMChild::SetTimer(int64_t aDelayMs, void* aContext)
 {
   MOZ_ASSERT(IsOnMessageLoopThread());
   GMP_LOG("ChromiumCDMChild::SetTimer(delay=%" PRId64 ", context=0x%p)",
           aDelayMs,
@@ -240,16 +341,21 @@ ChromiumCDMChild::CreateFileIO(cdm::File
   MOZ_ASSERT(IsOnMessageLoopThread());
   GMP_LOG("ChromiumCDMChild::CreateFileIO()");
   if (!mPersistentStateAllowed) {
     return nullptr;
   }
   return new WidevineFileIO(aClient);
 }
 
+ChromiumCDMChild::~ChromiumCDMChild()
+{
+  GMP_LOG("ChromiumCDMChild:: dtor this=%p", this);
+}
+
 bool
 ChromiumCDMChild::IsOnMessageLoopThread()
 {
   return mPlugin && mPlugin->GMPMessageLoop() == MessageLoop::current();
 }
 
 mozilla::ipc::IPCResult
 ChromiumCDMChild::RecvInit(const bool& aAllowDistinctiveIdentifier,
@@ -372,23 +478,16 @@ ChromiumCDMChild::RecvRemoveSession(cons
           aPromiseId,
           aSessionId.get());
   if (mCDM) {
     mCDM->RemoveSession(aPromiseId, aSessionId.get(), aSessionId.Length());
   }
   return IPC_OK();
 }
 
-void
-ChromiumCDMChild::DecryptFailed(uint32_t aId, cdm::Status aStatus)
-{
-  MOZ_ASSERT(IsOnMessageLoopThread());
-  Unused << SendDecrypted(aId, aStatus, nsTArray<uint8_t>());
-}
-
 static void
 InitInputBuffer(const CDMInputBuffer& aBuffer,
                 nsTArray<cdm::SubsampleEntry>& aSubSamples,
                 cdm::InputBuffer& aInputBuffer)
 {
   aInputBuffer.data = aBuffer.mData().get<uint8_t>();
   aInputBuffer.data_size = aBuffer.mData().Size<uint8_t>();
 
@@ -405,62 +504,94 @@ InitInputBuffer(const CDMInputBuffer& aB
                                                     aBuffer.mCipherBytes()[i]));
     }
     aInputBuffer.subsamples = aSubSamples.Elements();
     aInputBuffer.num_subsamples = aSubSamples.Length();
   }
   aInputBuffer.timestamp = aBuffer.mTimestamp();
 }
 
+bool
+ChromiumCDMChild::HasShmemOfSize(size_t aSize) const
+{
+  for (const ipc::Shmem& shmem : mBuffers) {
+    if (shmem.Size<uint8_t>() == aSize) {
+      return true;
+    }
+  }
+  return false;
+}
+
 mozilla::ipc::IPCResult
 ChromiumCDMChild::RecvDecrypt(const uint32_t& aId,
                               const CDMInputBuffer& aBuffer)
 {
   MOZ_ASSERT(IsOnMessageLoopThread());
   GMP_LOG("ChromiumCDMChild::RecvDecrypt()");
 
-  auto autoDeallocateShmem = MakeScopeExit([&,this] {
-    this->DeallocShmem(aBuffer.mData());
+  // Parent should have already gifted us a shmem to use as output.
+  size_t outputShmemSize = aBuffer.mData().Size<uint8_t>();
+  MOZ_ASSERT(HasShmemOfSize(outputShmemSize));
+
+  // Ensure we deallocate the shmem used to send input.
+  RefPtr<ChromiumCDMChild> self = this;
+  auto autoDeallocateInputShmem =
+    MakeScopeExit([&, self] { self->DeallocShmem(aBuffer.mData()); });
+
+  // On failure, we need to ensure that the shmem that the parent sent
+  // for the CDM to use to return output back to the parent is deallocated.
+  // Otherwise, it will leak.
+  auto autoDeallocateOutputShmem = MakeScopeExit([self, outputShmemSize] {
+    self->mBuffers.RemoveElementsBy([outputShmemSize, self](ipc::Shmem& aShmem) {
+      if (aShmem.Size<uint8_t>() != outputShmemSize) {
+        return false;
+      }
+      self->DeallocShmem(aShmem);
+      return true;
+    });
   });
 
   if (!mCDM) {
     GMP_LOG("ChromiumCDMChild::RecvDecrypt() no CDM");
-    DecryptFailed(aId, cdm::kDecryptError);
+    Unused << SendDecryptFailed(aId, cdm::kDecryptError);
     return IPC_OK();
   }
   if (aBuffer.mClearBytes().Length() != aBuffer.mCipherBytes().Length()) {
     GMP_LOG("ChromiumCDMChild::RecvDecrypt() clear/cipher bytes length doesn't "
             "match");
-    DecryptFailed(aId, cdm::kDecryptError);
+    Unused << SendDecryptFailed(aId, cdm::kDecryptError);
     return IPC_OK();
   }
 
   cdm::InputBuffer input;
   nsTArray<cdm::SubsampleEntry> subsamples;
   InitInputBuffer(aBuffer, subsamples, input);
 
   WidevineDecryptedBlock output;
   cdm::Status status = mCDM->Decrypt(input, &output);
 
-  if (status != cdm::kSuccess) {
-    DecryptFailed(aId, status);
+  // CDM should have allocated a cdm::Buffer for output.
+  CDMShmemBuffer* buffer =
+    output.DecryptedBuffer()
+      ? reinterpret_cast<CDMShmemBuffer*>(output.DecryptedBuffer())
+      : nullptr;
+  if (status != cdm::kSuccess || !buffer) {
+    Unused << SendDecryptFailed(aId, status);
     return IPC_OK();
   }
 
-  if (!output.DecryptedBuffer() ||
-      output.DecryptedBuffer()->Size() != aBuffer.mData().Size<uint8_t>()) {
-    // The sizes of the input and output should exactly match.
-    DecryptFailed(aId, cdm::kDecryptError);
-    return IPC_OK();
+  // Success! Return the decrypted sample to parent.
+  MOZ_ASSERT(!HasShmemOfSize(outputShmemSize));
+  ipc::Shmem shmem = buffer->ExtractShmem();
+  if (SendDecrypted(aId, cdm::kSuccess, shmem)) {
+    // No need to deallocate the output shmem; it should have been returned
+    // to the content process.
+    autoDeallocateOutputShmem.release();
   }
 
-  nsTArray<uint8_t> buf =
-    static_cast<WidevineBuffer*>(output.DecryptedBuffer())->ExtractBuffer();
-  Unused << SendDecrypted(aId, cdm::kSuccess, buf);
-
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
 ChromiumCDMChild::RecvInitializeVideoDecoder(
   const CDMVideoDecoderConfig& aConfig)
 {
   MOZ_ASSERT(IsOnMessageLoopThread());
@@ -488,16 +619,20 @@ ChromiumCDMChild::RecvDeinitializeVideoD
 {
   MOZ_ASSERT(IsOnMessageLoopThread());
   GMP_LOG("ChromiumCDMChild::RecvDeinitializeVideoDecoder()");
   MOZ_ASSERT(mDecoderInitialized);
   if (mDecoderInitialized) {
     mDecoderInitialized = false;
     mCDM->DeinitializeDecoder(cdm::kStreamTypeVideo);
   }
+  for (ipc::Shmem& shmem : mBuffers) {
+    DeallocShmem(shmem);
+  }
+  mBuffers.Clear();
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
 ChromiumCDMChild::RecvResetVideoDecoder()
 {
   MOZ_ASSERT(IsOnMessageLoopThread());
   GMP_LOG("ChromiumCDMChild::RecvResetVideoDecoder()");
@@ -510,19 +645,21 @@ ChromiumCDMChild::RecvResetVideoDecoder(
 
 mozilla::ipc::IPCResult
 ChromiumCDMChild::RecvDecryptAndDecodeFrame(const CDMInputBuffer& aBuffer)
 {
   MOZ_ASSERT(IsOnMessageLoopThread());
   GMP_LOG("ChromiumCDMChild::RecvDecryptAndDecodeFrame() t=%" PRId64 ")",
           aBuffer.mTimestamp());
   MOZ_ASSERT(mDecoderInitialized);
+  MOZ_ASSERT(!mBuffers.IsEmpty());
 
-  auto autoDeallocateShmem = MakeScopeExit([&, this] {
-    this->DeallocShmem(aBuffer.mData());
+  RefPtr<ChromiumCDMChild> self = this;
+  auto autoDeallocateShmem = MakeScopeExit([&, self] {
+    self->DeallocShmem(aBuffer.mData());
   });
 
   // The output frame may not have the same timestamp as the frame we put in.
   // We may need to input a number of frames before we receive output. The
   // CDM's decoder reorders to ensure frames output are in presentation order.
   // So we need to store the durations of the frames input, and retrieve them
   // on output.
   mFrameDurations.Insert(aBuffer.mTimestamp(), aBuffer.mDuration());
@@ -549,37 +686,37 @@ ChromiumCDMChild::RecvDecryptAndDecodeFr
       // to a stream that doesn't require OP. In order to keep the playback
       // pipeline rolling, just output a black frame. See bug 1343140.
       frame.InitToBlack(mCodedSize.width, mCodedSize.height, input.timestamp);
       MOZ_FALLTHROUGH;
     case cdm::kSuccess:
       ReturnOutput(frame);
       break;
     case cdm::kNeedMoreData:
-      Unused << SendDecoded(gmp::CDMVideoFrame());
+      Unused << SendDecodeFailed(rv);
       break;
     default:
       Unused << SendDecodeFailed(rv);
       break;
   }
 
   return IPC_OK();
 }
 
 void
 ChromiumCDMChild::ReturnOutput(WidevineVideoFrame& aFrame)
 {
-  // TODO: WidevineBuffers should hold a shmem instead of a array, and we can
-  // send the handle instead of copying the array here.
+  MOZ_ASSERT(IsOnMessageLoopThread());
   gmp::CDMVideoFrame output;
   output.mFormat() = static_cast<cdm::VideoFormat>(aFrame.Format());
   output.mImageWidth() = aFrame.Size().width;
   output.mImageHeight() = aFrame.Size().height;
-  output.mData() = Move(
-    reinterpret_cast<WidevineBuffer*>(aFrame.FrameBuffer())->ExtractBuffer());
+  CDMShmemBuffer* cdmSharedBuffer =
+    reinterpret_cast<CDMShmemBuffer*>(aFrame.FrameBuffer());
+  output.mData() = cdmSharedBuffer->ExtractShmem();
   output.mYPlane() = { aFrame.PlaneOffset(cdm::VideoFrame::kYPlane),
                        aFrame.Stride(cdm::VideoFrame::kYPlane) };
   output.mUPlane() = { aFrame.PlaneOffset(cdm::VideoFrame::kUPlane),
                        aFrame.Stride(cdm::VideoFrame::kUPlane) };
   output.mVPlane() = { aFrame.PlaneOffset(cdm::VideoFrame::kVPlane),
                        aFrame.Stride(cdm::VideoFrame::kVPlane) };
   output.mTimestamp() = aFrame.Timestamp();
 
@@ -589,16 +726,18 @@ ChromiumCDMChild::ReturnOutput(WidevineV
   }
 
   Unused << SendDecoded(output);
 }
 
 mozilla::ipc::IPCResult
 ChromiumCDMChild::RecvDrain()
 {
+  MOZ_ASSERT(IsOnMessageLoopThread());
+  MOZ_ASSERT(!mBuffers.IsEmpty());
   WidevineVideoFrame frame;
   cdm::InputBuffer sample;
   cdm::Status rv = mCDM->DecryptAndDecodeFrame(sample, &frame);
   CDM_LOG("ChromiumCDMChild::RecvDrain();  DecryptAndDecodeFrame() rv=%d", rv);
   if (rv == cdm::kSuccess) {
     MOZ_ASSERT(frame.Format() != cdm::kUnknownVideoFormat);
     ReturnOutput(frame);
   } else {
@@ -620,10 +759,32 @@ ChromiumCDMChild::RecvDestroy()
     mCDM = nullptr;
   }
 
   Unused << Send__delete__(this);
 
   return IPC_OK();
 }
 
+mozilla::ipc::IPCResult
+ChromiumCDMChild::RecvGiveBuffer(ipc::Shmem&& aBuffer)
+{
+  MOZ_ASSERT(IsOnMessageLoopThread());
+
+  GiveBuffer(Move(aBuffer));
+  return IPC_OK();
+}
+
+void
+ChromiumCDMChild::GiveBuffer(ipc::Shmem&& aBuffer)
+{
+  MOZ_ASSERT(IsOnMessageLoopThread());
+  size_t sz = aBuffer.Size<uint8_t>();
+  mBuffers.AppendElement(Move(aBuffer));
+  GMP_LOG("ChromiumCDMChild::RecvGiveBuffer(capacity=%" PRIuSIZE
+          ") bufferSizes={%s} mDecoderInitialized=%d",
+          sz,
+          ToString(mBuffers).get(),
+          mDecoderInitialized);
+}
+
 } // namespace gmp
 } // namespace mozilla
--- a/dom/media/gmp/ChromiumCDMChild.h
+++ b/dom/media/gmp/ChromiumCDMChild.h
@@ -68,21 +68,26 @@ public:
                              uint32_t aServiceIdSize,
                              const char* aChallenge,
                              uint32_t aChallengeSize) override {}
   void EnableOutputProtection(uint32_t aDesiredProtectionMask) override {}
   void QueryOutputProtectionStatus() override {}
   void OnDeferredInitializationDone(cdm::StreamType aStreamType,
                                     cdm::Status aDecoderStatus) override {}
   cdm::FileIO* CreateFileIO(cdm::FileIOClient* aClient) override;
+
+  void GiveBuffer(ipc::Shmem&& aBuffer);
+
 protected:
-  ~ChromiumCDMChild() {}
+  ~ChromiumCDMChild();
 
   bool IsOnMessageLoopThread();
 
+  ipc::IPCResult RecvGiveBuffer(ipc::Shmem&& aShmem) override;
+
   ipc::IPCResult RecvInit(const bool& aAllowDistinctiveIdentifier,
                           const bool& aAllowPersistentState) override;
   ipc::IPCResult RecvSetServerCertificate(
     const uint32_t& aPromiseId,
     nsTArray<uint8_t>&& aServerCert) override;
   ipc::IPCResult RecvCreateSessionAndGenerateRequest(
     const uint32_t& aPromiseId,
     const uint32_t& aSessionType,
@@ -104,27 +109,28 @@ protected:
     const CDMVideoDecoderConfig& aConfig) override;
   ipc::IPCResult RecvDeinitializeVideoDecoder() override;
   ipc::IPCResult RecvResetVideoDecoder() override;
   ipc::IPCResult RecvDecryptAndDecodeFrame(
     const CDMInputBuffer& aBuffer) override;
   ipc::IPCResult RecvDrain() override;
   ipc::IPCResult RecvDestroy() override;
 
-  void DecryptFailed(uint32_t aId, cdm::Status aStatus);
   void ReturnOutput(WidevineVideoFrame& aFrame);
+  bool HasShmemOfSize(size_t aSize) const;
 
   GMPContentChild* mPlugin = nullptr;
   cdm::ContentDecryptionModule_8* mCDM = nullptr;
 
   typedef SimpleMap<uint64_t> DurationMap;
   DurationMap mFrameDurations;
   nsTArray<uint32_t> mLoadSessionPromiseIds;
 
   cdm::Size mCodedSize;
+  nsTArray<ipc::Shmem> mBuffers;
 
   bool mDecoderInitialized = false;
   bool mPersistentStateAllowed = false;
 };
 
 } // namespace gmp
 } // namespace mozilla
 
--- a/dom/media/gmp/ChromiumCDMParent.cpp
+++ b/dom/media/gmp/ChromiumCDMParent.cpp
@@ -7,16 +7,18 @@
 #include "mozilla/gmp/GMPTypes.h"
 #include "GMPContentChild.h"
 #include "GMPContentParent.h"
 #include "mozilla/Unused.h"
 #include "ChromiumCDMProxy.h"
 #include "mozilla/dom/MediaKeyMessageEventBinding.h"
 #include "content_decryption_module.h"
 #include "GMPLog.h"
+#include "MediaPrefs.h"
+#include "GMPUtils.h"
 
 namespace mozilla {
 namespace gmp {
 
 ChromiumCDMParent::ChromiumCDMParent(GMPContentParent* aContentParent,
                                      uint32_t aPluginId)
   : mPluginId(aPluginId)
   , mContentParent(aContentParent)
@@ -165,17 +167,18 @@ ChromiumCDMParent::RemoveSession(const n
     RejectPromise(
       aPromiseId,
       NS_ERROR_DOM_INVALID_STATE_ERR,
       NS_LITERAL_CSTRING("Failed to send removeSession to CDM process"));
   }
 }
 
 bool
-ChromiumCDMParent::InitCDMInputBuffer(gmp::CDMInputBuffer& aBuffer, MediaRawData* aSample)
+ChromiumCDMParent::InitCDMInputBuffer(gmp::CDMInputBuffer& aBuffer,
+                                      MediaRawData* aSample)
 {
   const CryptoSample& crypto = aSample->mCrypto;
   if (crypto.mEncryptedSizes.Length() != crypto.mPlainSizes.Length()) {
     GMP_LOG("InitCDMInputBuffer clear/cipher subsamples don't match");
     return false;
   }
 
   Shmem shmem;
@@ -190,28 +193,50 @@ ChromiumCDMParent::InitCDMInputBuffer(gm
                                 aSample->mTime,
                                 aSample->mDuration,
                                 crypto.mPlainSizes,
                                 crypto.mEncryptedSizes,
                                 crypto.mValid);
   return true;
 }
 
+bool
+ChromiumCDMParent::SendBufferToCDM(uint32_t aSizeInBytes)
+{
+  Shmem shmem;
+  if (!AllocShmem(aSizeInBytes, Shmem::SharedMemory::TYPE_BASIC, &shmem)) {
+    return false;
+  }
+  if (!SendGiveBuffer(shmem)) {
+    DeallocShmem(shmem);
+    return false;
+  }
+  return true;
+}
+
 RefPtr<DecryptPromise>
 ChromiumCDMParent::Decrypt(MediaRawData* aSample)
 {
   if (mIsShutdown) {
     return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample),
                                            __func__);
   }
   CDMInputBuffer buffer;
   if (!InitCDMInputBuffer(buffer, aSample)) {
     return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample),
                                            __func__);
   }
+  // Send a buffer to the CDM to store the output. The CDM will either fill
+  // it with the decrypted sample and return it, or deallocate it on failure.
+  if (!SendBufferToCDM(aSample->Size())) {
+    DeallocShmem(buffer.mData());
+    return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample),
+                                           __func__);
+  }
+
   RefPtr<DecryptJob> job = new DecryptJob(aSample);
   if (!SendDecrypt(job->mId, buffer)) {
     GMP_LOG(
       "ChromiumCDMParent::Decrypt(this=%p) failed to send decrypt message",
       this);
     DeallocShmem(buffer.mData());
     return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample),
                                            __func__);
@@ -516,68 +541,99 @@ ToDecryptStatus(uint32_t aError)
     case cdm::kNoKey:
       return DecryptStatus::NoKeyErr;
     default:
       return DecryptStatus::GenericErr;
   }
 }
 
 ipc::IPCResult
+ChromiumCDMParent::RecvDecryptFailed(const uint32_t& aId,
+                                     const uint32_t& aStatus)
+{
+  GMP_LOG("ChromiumCDMParent::RecvDecryptFailed(this=%p, id=%u, status=%u)",
+          this,
+          aId,
+          aStatus);
+
+  if (mIsShutdown) {
+    MOZ_ASSERT(mDecrypts.IsEmpty());
+    return IPC_OK();
+  }
+
+  for (size_t i = 0; i < mDecrypts.Length(); i++) {
+    if (mDecrypts[i]->mId == aId) {
+      mDecrypts[i]->PostResult(ToDecryptStatus(aStatus));
+      mDecrypts.RemoveElementAt(i);
+      break;
+    }
+  }
+  return IPC_OK();
+}
+
+ipc::IPCResult
 ChromiumCDMParent::RecvDecrypted(const uint32_t& aId,
                                  const uint32_t& aStatus,
-                                 nsTArray<uint8_t>&& aData)
+                                 ipc::Shmem&& aShmem)
 {
   GMP_LOG("ChromiumCDMParent::RecvDecrypted(this=%p, id=%u, status=%u)",
           this,
           aId,
           aStatus);
+
+  // We must deallocate the shmem once we've copied the result out of it
+  // in PostResult below.
+  auto autoDeallocateShmem = MakeScopeExit([&, this] { DeallocShmem(aShmem); });
+
   if (mIsShutdown) {
     MOZ_ASSERT(mDecrypts.IsEmpty());
     return IPC_OK();
   }
   for (size_t i = 0; i < mDecrypts.Length(); i++) {
     if (mDecrypts[i]->mId == aId) {
-      mDecrypts[i]->PostResult(ToDecryptStatus(aStatus), aData);
+      mDecrypts[i]->PostResult(
+        ToDecryptStatus(aStatus),
+        MakeSpan<const uint8_t>(aShmem.get<uint8_t>(), aShmem.Size<uint8_t>()));
       mDecrypts.RemoveElementAt(i);
       break;
     }
   }
   return IPC_OK();
 }
 
 ipc::IPCResult
 ChromiumCDMParent::RecvDecoded(const CDMVideoFrame& aFrame)
 {
+  // On failure we need to deallocate the shmem used to store the decrypted
+  // sample. On success we return it to the CDM to be reused.
+  auto autoDeallocateShmem =
+    MakeScopeExit([&, this] { this->DeallocShmem(aFrame.mData()); });
+
   if (mIsShutdown || mDecodePromise.IsEmpty()) {
     return IPC_OK();
   }
   VideoData::YCbCrBuffer b;
-  nsTArray<uint8_t> data;
-  data = aFrame.mData();
+  uint8_t* data = aFrame.mData().get<uint8_t>();
+  MOZ_ASSERT(aFrame.mData().Size<uint8_t>() > 0);
 
-  if (data.IsEmpty()) {
-    mDecodePromise.ResolveIfExists(nsTArray<RefPtr<MediaData>>(), __func__);
-    return IPC_OK();
-  }
-
-  b.mPlanes[0].mData = data.Elements();
+  b.mPlanes[0].mData = data;
   b.mPlanes[0].mWidth = aFrame.mImageWidth();
   b.mPlanes[0].mHeight = aFrame.mImageHeight();
   b.mPlanes[0].mStride = aFrame.mYPlane().mStride();
   b.mPlanes[0].mOffset = aFrame.mYPlane().mPlaneOffset();
   b.mPlanes[0].mSkip = 0;
 
-  b.mPlanes[1].mData = data.Elements();
+  b.mPlanes[1].mData = data;
   b.mPlanes[1].mWidth = (aFrame.mImageWidth() + 1) / 2;
   b.mPlanes[1].mHeight = (aFrame.mImageHeight() + 1) / 2;
   b.mPlanes[1].mStride = aFrame.mUPlane().mStride();
   b.mPlanes[1].mOffset = aFrame.mUPlane().mPlaneOffset();
   b.mPlanes[1].mSkip = 0;
 
-  b.mPlanes[2].mData = data.Elements();
+  b.mPlanes[2].mData = data;
   b.mPlanes[2].mWidth = (aFrame.mImageWidth() + 1) / 2;
   b.mPlanes[2].mHeight = (aFrame.mImageHeight() + 1) / 2;
   b.mPlanes[2].mStride = aFrame.mVPlane().mStride();
   b.mPlanes[2].mOffset = aFrame.mVPlane().mPlaneOffset();
   b.mPlanes[2].mSkip = 0;
 
   gfx::IntRect pictureRegion(0, 0, aFrame.mImageWidth(), aFrame.mImageHeight());
   RefPtr<VideoData> v = VideoData::CreateAndCopyData(mVideoInfo,
@@ -585,17 +641,29 @@ ChromiumCDMParent::RecvDecoded(const CDM
                                                      mLastStreamOffset,
                                                      aFrame.mTimestamp(),
                                                      aFrame.mDuration(),
                                                      b,
                                                      false,
                                                      -1,
                                                      pictureRegion);
 
-  RefPtr<ChromiumCDMParent> self = this;
+  // Return the shmem to the CDM so the shmem can be reused to send us
+  // another frame.
+  if (!SendGiveBuffer(aFrame.mData())) {
+    mDecodePromise.RejectIfExists(
+      MediaResult(NS_ERROR_OUT_OF_MEMORY,
+                  RESULT_DETAIL("Can't return shmem to CDM process")),
+      __func__);
+    return IPC_OK();
+  }
+  // Don't need to deallocate the shmem since the CDM process is responsible
+  // for it again.
+  autoDeallocateShmem.release();
+
   if (v) {
     mDecodePromise.ResolveIfExists({ Move(v) }, __func__);
   } else {
     mDecodePromise.RejectIfExists(
       MediaResult(NS_ERROR_OUT_OF_MEMORY,
                   RESULT_DETAIL("CallBack::CreateAndCopyData")),
       __func__);
   }
@@ -605,16 +673,22 @@ ChromiumCDMParent::RecvDecoded(const CDM
 
 ipc::IPCResult
 ChromiumCDMParent::RecvDecodeFailed(const uint32_t& aStatus)
 {
   if (mIsShutdown) {
     MOZ_ASSERT(mDecodePromise.IsEmpty());
     return IPC_OK();
   }
+
+  if (aStatus == cdm::kNeedMoreData) {
+    mDecodePromise.ResolveIfExists(nsTArray<RefPtr<MediaData>>(), __func__);
+    return IPC_OK();
+  }
+
   mDecodePromise.RejectIfExists(
     MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                 RESULT_DETAIL("ChromiumCDMParent::RecvDecodeFailed")),
     __func__);
   return IPC_OK();
 }
 
 ipc::IPCResult
@@ -659,41 +733,89 @@ ChromiumCDMParent::InitializeVideoDecode
 {
   if (mIsShutdown) {
     return MediaDataDecoder::InitPromise::CreateAndReject(
       MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                   RESULT_DETAIL("ChromiumCDMParent is shutdown")),
       __func__);
   }
 
+  const int32_t bufferSize =
+    I420FrameBufferSizePadded(aInfo.mImage.width, aInfo.mImage.height);
+  if (bufferSize <= 0) {
+    return MediaDataDecoder::InitPromise::CreateAndReject(
+      MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+                  RESULT_DETAIL("Video frame buffer size is invalid.")),
+      __func__);
+  }
+
   if (!SendInitializeVideoDecoder(aConfig)) {
     return MediaDataDecoder::InitPromise::CreateAndReject(
       MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                   RESULT_DETAIL("Failed to send init video decoder to CDM")),
       __func__);
   }
 
   mVideoDecoderInitialized = true;
   mImageContainer = aImageContainer;
   mVideoInfo = aInfo;
+  mVideoFrameBufferSize = bufferSize;
 
   return mInitVideoDecoderPromise.Ensure(__func__);
 }
 
 ipc::IPCResult
 ChromiumCDMParent::RecvOnDecoderInitDone(const uint32_t& aStatus)
 {
   GMP_LOG("ChromiumCDMParent::RecvOnDecoderInitDone(this=%p, status=%u)",
           this,
           aStatus);
   if (mIsShutdown) {
     MOZ_ASSERT(mInitVideoDecoderPromise.IsEmpty());
     return IPC_OK();
   }
   if (aStatus == static_cast<uint32_t>(cdm::kSuccess)) {
+    // The Chromium CDM API requires us to implement a synchronous
+    // interface to supply buffers to the CDM for it to write decrypted samples
+    // into. We want our buffers to be backed by shmems, in order to reduce
+    // the overhead of transferring decoded frames. However due to sandboxing
+    // restrictions, the CDM process cannot allocate shmems itself.
+    // We don't want to be doing synchronous IPC to request shmems from the
+    // content process, nor do we want to have to do intr IPC or make async
+    // IPC conform to the sync allocation interface. So instead we have the
+    // content process pre-allocate a set of shmems and give them to the CDM
+    // process in advance of them being needed.
+    //
+    // When the CDM needs to allocate a buffer for storing a decrypted sample,
+    // the CDM host gives it one of these shmems' buffers. When this is sent
+    // back to the content process, we copy the result out (uploading to a
+    // GPU surface for video frames), and send the shmem back to the CDM
+    // process so it can reuse it.
+    //
+    // We predict the size of buffer the CDM will allocate, and prepopulate
+    // the CDM's list of shmems with shmems of at least that size, plus a bit
+    // of padding for safety.
+    //
+    // Normally the CDM won't allocate more than one buffer at once, but
+    // we've seen cases where it allocates two buffers, returns one and holds
+    // onto the other. So the minimum number of shmems we give to the CDM
+    // must be at least two, and the default is three for safety.
+    const uint32_t count =
+      std::max<uint32_t>(2u, MediaPrefs::EMEChromiumAPIVideoShmemCount());
+    for (uint32_t i = 0; i < count; i++) {
+      if (!SendBufferToCDM(mVideoFrameBufferSize)) {
+        mVideoDecoderInitialized = false;
+        mInitVideoDecoderPromise.RejectIfExists(
+          MediaResult(
+            NS_ERROR_DOM_MEDIA_FATAL_ERR,
+            RESULT_DETAIL("Failled to send shmems to CDM after decode init.")),
+          __func__);
+        return IPC_OK();
+      }
+    }
     mInitVideoDecoderPromise.ResolveIfExists(TrackInfo::kVideoTrack, __func__);
   } else {
     mVideoDecoderInitialized = false;
     mInitVideoDecoderPromise.RejectIfExists(
       MediaResult(
         NS_ERROR_DOM_MEDIA_FATAL_ERR,
         RESULT_DETAIL("CDM init decode failed with %" PRIu32, aStatus)),
       __func__);
@@ -706,16 +828,19 @@ ChromiumCDMParent::DecryptAndDecodeFrame
 {
   if (mIsShutdown) {
     return MediaDataDecoder::DecodePromise::CreateAndReject(
       MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                   RESULT_DETAIL("ChromiumCDMParent is shutdown")),
       __func__);
   }
 
+  GMP_LOG("ChromiumCDMParent::DecryptAndDecodeFrame t=%" PRId64,
+          aSample->mTime);
+
   CDMInputBuffer buffer;
 
   if (!InitCDMInputBuffer(buffer, aSample)) {
     return MediaDataDecoder::DecodePromise::CreateAndReject(
       MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Failed to init CDM buffer."),
       __func__);
   }
 
--- a/dom/media/gmp/ChromiumCDMParent.h
+++ b/dom/media/gmp/ChromiumCDMParent.h
@@ -107,24 +107,27 @@ protected:
     const double& aSecondsSinceEpoch) override;
   ipc::IPCResult RecvOnSessionClosed(const nsCString& aSessionId) override;
   ipc::IPCResult RecvOnLegacySessionError(const nsCString& aSessionId,
                                           const uint32_t& aError,
                                           const uint32_t& aSystemCode,
                                           const nsCString& aMessage) override;
   ipc::IPCResult RecvDecrypted(const uint32_t& aId,
                                const uint32_t& aStatus,
-                               nsTArray<uint8_t>&& aData) override;
+                               ipc::Shmem&& aData) override;
+  ipc::IPCResult RecvDecryptFailed(const uint32_t& aId,
+                                   const uint32_t& aStatus) override;
   ipc::IPCResult RecvOnDecoderInitDone(const uint32_t& aStatus) override;
   ipc::IPCResult RecvDecoded(const CDMVideoFrame& aFrame) override;
   ipc::IPCResult RecvDecodeFailed(const uint32_t& aStatus) override;
   ipc::IPCResult RecvShutdown() override;
   ipc::IPCResult RecvResetVideoDecoderComplete() override;
   ipc::IPCResult RecvDrainComplete() override;
   void ActorDestroy(ActorDestroyReason aWhy) override;
+  bool SendBufferToCDM(uint32_t aSizeInBytes);
 
   void RejectPromise(uint32_t aPromiseId,
                      nsresult aError,
                      const nsCString& aErrorMessage);
 
   void ResolvePromise(uint32_t aPromiseId);
 
   bool InitCDMInputBuffer(gmp::CDMInputBuffer& aBuffer, MediaRawData* aSample);
@@ -142,16 +145,18 @@ protected:
   MozPromiseHolder<MediaDataDecoder::DecodePromise> mDecodePromise;
 
   RefPtr<layers::ImageContainer> mImageContainer;
   VideoInfo mVideoInfo;
   uint64_t mLastStreamOffset = 0;
 
   MozPromiseHolder<MediaDataDecoder::FlushPromise> mFlushDecoderPromise;
 
+  int32_t mVideoFrameBufferSize = 0;
+
   bool mIsShutdown = false;
   bool mVideoDecoderInitialized = false;
   bool mActorDestroyed = false;
 };
 
 } // namespace gmp
 } // namespace mozilla
 
--- a/dom/media/gmp/GMPTypes.ipdlh
+++ b/dom/media/gmp/GMPTypes.ipdlh
@@ -90,17 +90,17 @@ struct CDMVideoPlane {
   uint32_t mPlaneOffset;
   uint32_t mStride;
 };
 
 struct CDMVideoFrame {
   uint32_t mFormat;
   int32_t mImageWidth;
   int32_t mImageHeight;
-  uint8_t[] mData;
+  Shmem mData;
   CDMVideoPlane mYPlane;
   CDMVideoPlane mUPlane;
   CDMVideoPlane mVPlane;
   int64_t mTimestamp;
   int64_t mDuration;
 };
 
 }
--- a/dom/media/gmp/GMPUtils.cpp
+++ b/dom/media/gmp/GMPUtils.cpp
@@ -234,9 +234,28 @@ LogToConsole(const nsAString& aMsg)
 RefPtr<AbstractThread>
 GetGMPAbstractThread()
 {
   RefPtr<gmp::GeckoMediaPluginService> service =
     gmp::GeckoMediaPluginService::GetGeckoMediaPluginService();
   return service ? service->GetAbstractGMPThread() : nullptr;
 }
 
+static int32_t
+Align16(int32_t aNumber)
+{
+  const uint32_t mask = 15; // Alignment - 1.
+  return (aNumber + mask) & ~mask;
+}
+
+int32_t
+I420FrameBufferSizePadded(int32_t aWidth, int32_t aHeight)
+{
+  if (aWidth <= 0 || aHeight <= 0 || aWidth > MAX_VIDEO_WIDTH ||
+      aHeight > MAX_VIDEO_HEIGHT) {
+    return 0;
+  }
+
+  int32_t ySize = Align16(aWidth) * Align16(aHeight);
+  return ySize + (ySize / 4) * 2;
+}
+
 } // namespace mozilla
--- a/dom/media/gmp/GMPUtils.h
+++ b/dom/media/gmp/GMPUtils.h
@@ -83,11 +83,16 @@ HaveGMPFor(const nsCString& aAPI,
            nsTArray<nsCString>&& aTags);
 
 void
 LogToConsole(const nsAString& aMsg);
 
 RefPtr<AbstractThread>
 GetGMPAbstractThread();
 
+// Returns the number of bytes required to store an aWidth x aHeight image in
+// I420 format, padded so that the width and height are multiples of 16.
+int32_t
+I420FrameBufferSizePadded(int32_t aWidth, int32_t aHeight);
+
 } // namespace mozilla
 
 #endif
--- a/dom/media/gmp/PChromiumCDM.ipdl
+++ b/dom/media/gmp/PChromiumCDM.ipdl
@@ -49,16 +49,18 @@ child:
   async ResetVideoDecoder();
 
   async DecryptAndDecodeFrame(CDMInputBuffer aBuffer);
 
   async Drain();
 
   async Destroy();
 
+  async GiveBuffer(Shmem aShmem);
+
 parent:
   async __delete__();
 
   // cdm::Host8
   async OnResolveNewSessionPromise(uint32_t aPromiseId, nsCString aSessionId);
 
   async OnResolvePromise(uint32_t aPromiseId);
 
@@ -82,17 +84,18 @@ parent:
   async OnLegacySessionError(nsCString aSessionId,
                              uint32_t aError,
                              uint32_t aSystemCode,
                              nsCString aMessage);
 
   async ResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccessful);
 
   // Return values of cdm::ContentDecryptionModule8::Decrypt
-  async Decrypted(uint32_t aId, uint32_t aStatus, Shmem aData);
+  async Decrypted(uint32_t aId, uint32_t aStatus, Shmem aDecryptedData);
+  async DecryptFailed(uint32_t aId, uint32_t aStatus);
 
   async OnDecoderInitDone(uint32_t aStatus);
 
   // Return values of cdm::ContentDecryptionModule8::DecryptAndDecodeFrame
   async Decoded(CDMVideoFrame aFrame);
   async DecodeFailed(uint32_t aStatus);
 
   async ResetVideoDecoderComplete();
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -360,16 +360,17 @@ pref("media.gmp.decoder.h264", 0);
 pref("media.raw.enabled", true);
 #endif
 pref("media.ogg.enabled", true);
 pref("media.opus.enabled", true);
 pref("media.wave.enabled", true);
 pref("media.webm.enabled", true);
 
 pref("media.eme.chromium-api.enabled", false);
+pref("media.eme.chromium-api.video-shmems", 3);
 
 #ifdef MOZ_APPLEMEDIA
 #ifdef MOZ_WIDGET_UIKIT
 pref("media.mp3.enabled", true);
 #endif
 pref("media.apple.mp3.enabled", true);
 pref("media.apple.mp4.enabled", true);
 #endif