Bug 1358373 - Handle underestimating how many shmems the CDM needs to return decoded video frames to Gecko. r=gerald draft
authorChris Pearce <cpearce@mozilla.com>
Wed, 26 Apr 2017 15:46:09 +1200
changeset 569161 92870f1adc7ae68e58b15443e4223012bdf0e39a
parent 568509 0f5ba06c4c5959030a05cb852656d854065e2226
child 626120 8feda6c28a50df0d67af0e6864966ef08e0466b7
push id56080
push userbmo:cpearce@mozilla.com
push dateThu, 27 Apr 2017 02:56:10 +0000
reviewersgerald
bugs1358373
milestone55.0a1
Bug 1358373 - Handle underestimating how many shmems the CDM needs to return decoded video frames to Gecko. r=gerald The CDM process can't allocate shmems itself because it's sandboxed, so we pre-allocate shmems in the content process and send them to the GMP process for the CDM to use. Some sites seem to have encoded their content in such a way as to cause the CDM to allocate and hang onto more frames than we pre-allocate; so we run out of shmems in the GMP process, and the CDM can't allocate further buffers to return output, and we fail. So change the ChromiumCDMChild to allocate non-shmem-backed buffers if it runs out of shmems, and return the result to the content process as an nsTArray. Upon receiving that, the parent will send an extra shmem to the child, to hopefully avoid the slow path again. Also increase media.eme.chromium-api.video-shmems to 4, so that we're less likely to hit this slow path in the wild. I've seen that Lightbox.co.nz and Microsoft's EME test-drive require 4 shmems. MozReview-Commit-ID: ISQYDkTj5uY
dom/media/gmp/ChromiumCDMChild.cpp
dom/media/gmp/ChromiumCDMParent.cpp
dom/media/gmp/ChromiumCDMParent.h
dom/media/gmp/GMPTypes.ipdlh
dom/media/gmp/PChromiumCDM.ipdl
dom/media/gmp/widevine-adapter/WidevineUtils.h
modules/libpref/init/all.js
testing/profiles/prefs_general.js
--- a/dom/media/gmp/ChromiumCDMChild.cpp
+++ b/dom/media/gmp/ChromiumCDMChild.cpp
@@ -40,28 +40,39 @@ 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
+class CDMShmemBuffer : public CDMBuffer
 {
 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(ChromiumCDMChild* aProtocol,
+                 ipc::Shmem aShmem,
+                 WidevineBuffer* aLocalBuffer)
+    : CDMShmemBuffer(aProtocol, aShmem)
+  {
+    MOZ_ASSERT(aLocalBuffer->Size() == Size());
+    memcpy(Data(),
+           aLocalBuffer->Data(),
+           std::min<uint32_t>(aLocalBuffer->Size(), Size()));
+  }
+
   ~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.
@@ -92,16 +103,18 @@ public:
 
   ipc::Shmem ExtractShmem()
   {
     ipc::Shmem shmem = mShmem;
     mShmem = ipc::Shmem();
     return shmem;
   }
 
+  CDMShmemBuffer* AsShmemBuffer() override { return this; }
+
 private:
   RefPtr<ChromiumCDMChild> mProtocol;
   uint32_t mSize;
   mozilla::ipc::Shmem mShmem;
   CDMShmemBuffer(const CDMShmemBuffer&);
   void operator=(const CDMShmemBuffer&);
 };
 
@@ -138,19 +151,20 @@ ChromiumCDMChild::Allocate(uint32_t aCap
   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;
+    // size, but did not! Do a "dive and catch", and create an non-shared
+    // memory buffer. The parent will detect this and send us an extra shmem
+    // so future frames can be in shmems, i.e. returned on the fast path.
+    return new WidevineBuffer(aCapacity);
   }
   ipc::Shmem shmem = mBuffers[best];
   mBuffers.RemoveElementAt(best);
   return new CDMShmemBuffer(this, shmem);
 }
 
 void
 ChromiumCDMChild::SetTimer(int64_t aDelayMs, void* aContext)
@@ -566,18 +580,19 @@ ChromiumCDMChild::RecvDecrypt(const uint
   InitInputBuffer(aBuffer, subsamples, input);
 
   WidevineDecryptedBlock output;
   cdm::Status status = mCDM->Decrypt(input, &output);
 
   // CDM should have allocated a cdm::Buffer for output.
   CDMShmemBuffer* buffer =
     output.DecryptedBuffer()
-      ? reinterpret_cast<CDMShmemBuffer*>(output.DecryptedBuffer())
+      ? static_cast<CDMShmemBuffer*>(output.DecryptedBuffer())
       : nullptr;
+  MOZ_ASSERT(buffer->AsShmemBuffer());
   if (status != cdm::kSuccess || !buffer) {
     Unused << SendDecryptFailed(aId, status);
     return IPC_OK();
   }
 
   // Success! Return the decrypted sample to parent.
   MOZ_ASSERT(!HasShmemOfSize(outputShmemSize));
   ipc::Shmem shmem = buffer->ExtractShmem();
@@ -645,17 +660,16 @@ 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());
 
   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
@@ -671,73 +685,81 @@ ChromiumCDMChild::RecvDecryptAndDecodeFr
   WidevineVideoFrame frame;
   cdm::Status rv = mCDM->DecryptAndDecodeFrame(input, &frame);
   GMP_LOG("ChromiumCDMChild::RecvDecryptAndDecodeFrame() t=%" PRId64
           " CDM decoder rv=%d",
           aBuffer.mTimestamp(),
           rv);
 
   switch (rv) {
+    case cdm::kNeedMoreData:
+      Unused << SendDecodeFailed(rv);
+      break;
     case cdm::kNoKey:
       GMP_LOG("NoKey for sample at time=%" PRId64 "!", input.timestamp);
       // Somehow our key became unusable. Typically this would happen when
       // a stream requires output protection, and the configuration changed
       // such that output protection is no longer available. For example, a
       // non-compliant monitor was attached. The JS player should notice the
       // key status changing to "output-restricted", and is supposed to switch
       // 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 << SendDecodeFailed(rv);
-      break;
+      if (frame.FrameBuffer()) {
+        ReturnOutput(frame);
+        break;
+      }
+      // CDM didn't set a frame buffer on the sample, report it as an error.
+      MOZ_FALLTHROUGH;
     default:
       Unused << SendDecodeFailed(rv);
       break;
   }
 
   return IPC_OK();
 }
 
 void
 ChromiumCDMChild::ReturnOutput(WidevineVideoFrame& aFrame)
 {
   MOZ_ASSERT(IsOnMessageLoopThread());
+  MOZ_ASSERT(aFrame.FrameBuffer());
   gmp::CDMVideoFrame output;
   output.mFormat() = static_cast<cdm::VideoFormat>(aFrame.Format());
   output.mImageWidth() = aFrame.Size().width;
   output.mImageHeight() = aFrame.Size().height;
-  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();
 
   uint64_t duration = 0;
   if (mFrameDurations.Find(aFrame.Timestamp(), duration)) {
     output.mDuration() = duration;
   }
 
-  Unused << SendDecoded(output);
+  CDMBuffer* base = reinterpret_cast<CDMBuffer*>(aFrame.FrameBuffer());
+  if (base->AsShmemBuffer()) {
+    ipc::Shmem shmem = base->AsShmemBuffer()->ExtractShmem();
+    Unused << SendDecodedShmem(output, shmem);
+  } else {
+    MOZ_ASSERT(base->AsArrayBuffer());
+    Unused << SendDecodedData(output, base->AsArrayBuffer()->ExtractBuffer());
+  }
 }
 
 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 {
--- a/dom/media/gmp/ChromiumCDMParent.cpp
+++ b/dom/media/gmp/ChromiumCDMParent.cpp
@@ -17,16 +17,17 @@
 
 namespace mozilla {
 namespace gmp {
 
 ChromiumCDMParent::ChromiumCDMParent(GMPContentParent* aContentParent,
                                      uint32_t aPluginId)
   : mPluginId(aPluginId)
   , mContentParent(aContentParent)
+  , mVideoShmemCount(MediaPrefs::EMEChromiumAPIVideoShmemCount())
 {
   GMP_LOG(
     "ChromiumCDMParent::ChromiumCDMParent(this=%p, contentParent=%p, id=%u)",
     this,
     aContentParent,
     aPluginId);
 }
 
@@ -595,45 +596,92 @@ ChromiumCDMParent::RecvDecrypted(const u
       mDecrypts.RemoveElementAt(i);
       break;
     }
   }
   return IPC_OK();
 }
 
 ipc::IPCResult
-ChromiumCDMParent::RecvDecoded(const CDMVideoFrame& aFrame)
+ChromiumCDMParent::RecvDecodedData(const CDMVideoFrame& aFrame,
+                                   nsTArray<uint8_t>&& aData)
 {
-  // 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.
+  GMP_LOG("ChromiumCDMParent::RecvDecodedData(this=%p) "
+          "mVideoShmemCount=%" PRIu32,
+          this,
+          mVideoShmemCount);
+  // We'd expect CDMs to not have video frames larger than 1280x720 (due to
+  // DRM robustness requirements), which is about 1.5MB per frame. So put an
+  // upper limit on the number of shmems we tolerate the CDM asking for. In
+  // practice, we expect the CDM to need less than 5, but some encodings
+  // require more.
+  Shmem shmem;
+  if (mVideoShmemCount >= 50 || !AllocShmem(mVideoFrameBufferSize,
+                                            Shmem::SharedMemory::TYPE_BASIC,
+                                            &shmem)) {
+    GMP_LOG("ChromiumCDMParent::RecvDecodedData(this=%p) "
+            "failed to allocate shmem for CDM.",
+            this);
+    mVideoDecoderInitialized = false;
+    mDecodePromise.RejectIfExists(
+      MediaResult(
+        NS_ERROR_DOM_MEDIA_FATAL_ERR,
+        RESULT_DETAIL("Failled to send shmems to CDM after decode init.")),
+      __func__);
+    return IPC_OK();
+  }
+  mVideoShmemCount++;
+
+  ProcessDecoded(aFrame, aData, Move(shmem));
+
+  return IPC_OK();
+}
+
+ipc::IPCResult
+ChromiumCDMParent::RecvDecodedShmem(const CDMVideoFrame& aFrame,
+                                    ipc::Shmem&& aShmem)
+{
+  ProcessDecoded(
+    aFrame,
+    MakeSpan<uint8_t>(aShmem.get<uint8_t>(), aShmem.Size<uint8_t>()),
+    Move(aShmem));
+  return IPC_OK();
+}
+
+void
+ChromiumCDMParent::ProcessDecoded(const CDMVideoFrame& aFrame,
+                                  Span<uint8_t> aData,
+                                  ipc::Shmem&& aGiftShmem)
+{
+  // On failure we need to deallocate the shmem we're to return to the
+  // CDM. On success we return it to the CDM to be reused.
   auto autoDeallocateShmem =
-    MakeScopeExit([&, this] { this->DeallocShmem(aFrame.mData()); });
+    MakeScopeExit([&, this] { this->DeallocShmem(aGiftShmem); });
 
   if (mIsShutdown || mDecodePromise.IsEmpty()) {
-    return IPC_OK();
+    return;
   }
   VideoData::YCbCrBuffer b;
-  uint8_t* data = aFrame.mData().get<uint8_t>();
-  MOZ_ASSERT(aFrame.mData().Size<uint8_t>() > 0);
+  MOZ_ASSERT(aData.Length() > 0);
 
-  b.mPlanes[0].mData = data;
+  b.mPlanes[0].mData = aData.Elements();
   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;
+  b.mPlanes[1].mData = aData.Elements();
   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;
+  b.mPlanes[2].mData = aData.Elements();
   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(
@@ -644,37 +692,35 @@ ChromiumCDMParent::RecvDecoded(const CDM
     media::TimeUnit::FromMicroseconds(aFrame.mDuration()),
     b,
     false,
     -1,
     pictureRegion);
 
   // Return the shmem to the CDM so the shmem can be reused to send us
   // another frame.
-  if (!SendGiveBuffer(aFrame.mData())) {
+  if (!SendGiveBuffer(aGiftShmem)) {
     mDecodePromise.RejectIfExists(
       MediaResult(NS_ERROR_OUT_OF_MEMORY,
                   RESULT_DETAIL("Can't return shmem to CDM process")),
       __func__);
-    return IPC_OK();
+    return;
   }
   // 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__);
   }
-
-  return IPC_OK();
 }
 
 ipc::IPCResult
 ChromiumCDMParent::RecvDecodeFailed(const uint32_t& aStatus)
 {
   if (mIsShutdown) {
     MOZ_ASSERT(mDecodePromise.IsEmpty());
     return IPC_OK();
@@ -791,22 +837,29 @@ ChromiumCDMParent::RecvOnDecoderInitDone
     // 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++) {
+    // we've seen cases where it allocates multiple buffers, returns one and
+    // holds onto the rest. So we need to ensure we have a minimum number of
+    // shmems pre-allocated for the CDM. This minimum is set by the pref
+    // media.eme.chromium-api.video-shmems.
+    //
+    // We also have a failure recovery mechanism; if the CDM asks for more
+    // buffers than we have shmem's available, ChromiumCDMChild gives the
+    // CDM a non-shared memory buffer, and returns the frame to the parent
+    // in an nsTArray<uint8_t> instead of a shmem. Every time this happens,
+    // the parent sends an extra shmem to the CDM process for it to add to the
+    // set of shmems with which to return output. Via this mechanism we should
+    // recover from incorrectly predicting how many shmems to pre-allocate.
+    for (uint32_t i = 0; i < mVideoShmemCount; 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();
--- a/dom/media/gmp/ChromiumCDMParent.h
+++ b/dom/media/gmp/ChromiumCDMParent.h
@@ -10,16 +10,17 @@
 #include "GMPCrashHelper.h"
 #include "GMPCrashHelperHolder.h"
 #include "GMPMessageUtils.h"
 #include "mozilla/gmp/PChromiumCDMParent.h"
 #include "mozilla/RefPtr.h"
 #include "nsDataHashtable.h"
 #include "PlatformDecoderModule.h"
 #include "ImageContainer.h"
+#include "mozilla/Span.h"
 
 namespace mozilla {
 
 class MediaRawData;
 class ChromiumCDMProxy;
 
 namespace gmp {
 
@@ -111,17 +112,25 @@ protected:
                                           const uint32_t& aSystemCode,
                                           const nsCString& aMessage) override;
   ipc::IPCResult RecvDecrypted(const uint32_t& aId,
                                const uint32_t& aStatus,
                                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 RecvDecodedShmem(const CDMVideoFrame& aFrame,
+                                  ipc::Shmem&& aShmem) override;
+  ipc::IPCResult RecvDecodedData(const CDMVideoFrame& aFrame,
+                                 nsTArray<uint8_t>&& aData) override;
+
+  void ProcessDecoded(const CDMVideoFrame& aFrame,
+                      Span<uint8_t> aData,
+                      ipc::Shmem&& aGiftShmem);
+
   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,
@@ -147,16 +156,20 @@ protected:
   RefPtr<layers::ImageContainer> mImageContainer;
   VideoInfo mVideoInfo;
   uint64_t mLastStreamOffset = 0;
 
   MozPromiseHolder<MediaDataDecoder::FlushPromise> mFlushDecoderPromise;
 
   int32_t mVideoFrameBufferSize = 0;
 
+  // Count of the number of shmems in the set used to return decoded video
+  // frames from the CDM to Gecko.
+  uint32_t mVideoShmemCount;
+
   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,16 @@ struct CDMVideoPlane {
   uint32_t mPlaneOffset;
   uint32_t mStride;
 };
 
 struct CDMVideoFrame {
   uint32_t mFormat;
   int32_t mImageWidth;
   int32_t mImageHeight;
-  Shmem mData;
   CDMVideoPlane mYPlane;
   CDMVideoPlane mUPlane;
   CDMVideoPlane mVPlane;
   int64_t mTimestamp;
   int64_t mDuration;
 };
 
 }
--- a/dom/media/gmp/PChromiumCDM.ipdl
+++ b/dom/media/gmp/PChromiumCDM.ipdl
@@ -90,17 +90,18 @@ parent:
 
   // Return values of cdm::ContentDecryptionModule8::Decrypt
   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 DecodedShmem(CDMVideoFrame aFrame, Shmem aData);
+  async DecodedData(CDMVideoFrame aFrame, uint8_t[] aData);
   async DecodeFailed(uint32_t aStatus);
 
   async ResetVideoDecoderComplete();
 
   async DrainComplete();
 
   async Shutdown();
 };
--- a/dom/media/gmp/widevine-adapter/WidevineUtils.h
+++ b/dom/media/gmp/widevine-adapter/WidevineUtils.h
@@ -56,31 +56,47 @@ private:
 
 void InitInputBuffer(const GMPEncryptedBufferMetadata* aCrypto,
                      int64_t aTimestamp,
                      const uint8_t* aData,
                      size_t aDataSize,
                      cdm::InputBuffer &aInputBuffer,
                      nsTArray<cdm::SubsampleEntry> &aSubsamples);
 
-class WidevineBuffer : public cdm::Buffer
+namespace gmp {
+class CDMShmemBuffer;
+}
+class WidevineBuffer;
+
+// Base class for our cdm::Buffer implementations, so we can tell at runtime
+// whether the buffer is a Shmem or non-Shmem buffer.
+class CDMBuffer : public cdm::Buffer
+{
+public:
+  virtual WidevineBuffer* AsArrayBuffer() { return nullptr; }
+  virtual gmp::CDMShmemBuffer* AsShmemBuffer() { return nullptr; }
+};
+
+class WidevineBuffer : public CDMBuffer
 {
 public:
   explicit WidevineBuffer(size_t aSize);
   ~WidevineBuffer() override;
   void Destroy() override;
   uint32_t Capacity() const override;
   uint8_t* Data() override;
   void SetSize(uint32_t aSize) override;
   uint32_t Size() const override;
 
   // Moves contents of buffer out into temporary.
   // Note: This empties the buffer.
   nsTArray<uint8_t> ExtractBuffer();
 
+  WidevineBuffer* AsArrayBuffer() override { return this; }
+
 private:
   nsTArray<uint8_t> mBuffer;
   WidevineBuffer(const WidevineBuffer&);
   void operator=(const WidevineBuffer&);
 };
 
 class WidevineDecryptedBlock : public cdm::DecryptedBlock
 {
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -361,17 +361,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", true);
-pref("media.eme.chromium-api.video-shmems", 3);
+pref("media.eme.chromium-api.video-shmems", 4);
 
 #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
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -305,16 +305,21 @@ user_pref("browser.search.countryCode", 
 user_pref("browser.search.geoSpecificDefaults", false);
 
 // Make sure self support doesn't hit the network.
 user_pref("browser.selfsupport.url", "https://example.com/selfsupport-dummy/");
 user_pref("extensions.shield-recipe-client.api_url", "https://example.com/selfsupport-dummy/");
 
 user_pref("media.eme.enabled", true);
 
+// Set the number of shmems the PChromiumCDM protocol pre-allocates to 0,
+// so that we test the case where we under-estimate how many shmems we need
+// to send decoded video frames from the CDM to Gecko.
+user_pref("media.eme.chromium-api.video-shmems", 0);
+
 user_pref("media.autoplay.enabled", true);
 
 // Don't use auto-enabled e10s
 user_pref("browser.tabs.remote.autostart.1", false);
 user_pref("browser.tabs.remote.autostart.2", false);
 // Don't show a delay when hiding the audio indicator during tests
 user_pref("browser.tabs.delayHidingAudioPlayingIconMS", 0);
 // Don't forceably kill content processes after a timeout