Bug 1161913 - Part 2. Request canvas to push out its next drawn frame instead of pulling it. r=mt
authorAndreas Pehrson <pehrsons@gmail.com>
Thu, 17 Sep 2015 12:36:57 +0800
changeset 295809 444d8a31e89310e69a778d4df369d7e3cff098df
parent 295808 261ea38e6cf60b70a4cf548101de424e0f8cd90d
child 295810 c36e4b73500652bae7f71ffe9382b2ee000068c2
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmt
bugs1161913
milestone43.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 1161913 - Part 2. Request canvas to push out its next drawn frame instead of pulling it. r=mt
dom/html/HTMLCanvasElement.cpp
dom/html/HTMLCanvasElement.h
dom/media/CanvasCaptureMediaStream.cpp
dom/media/CanvasCaptureMediaStream.h
--- a/dom/html/HTMLCanvasElement.cpp
+++ b/dom/html/HTMLCanvasElement.cpp
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/HTMLCanvasElement.h"
 
 #include "ImageEncoder.h"
 #include "jsapi.h"
 #include "jsfriendapi.h"
 #include "Layers.h"
+#include "MediaSegment.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/Base64.h"
 #include "mozilla/CheckedInt.h"
 #include "mozilla/dom/CanvasCaptureMediaStream.h"
 #include "mozilla/dom/CanvasRenderingContext2D.h"
 #include "mozilla/dom/File.h"
 #include "mozilla/dom/HTMLCanvasElementBinding.h"
 #include "mozilla/dom/MouseEvent.h"
@@ -30,29 +31,160 @@
 #include "nsIScriptSecurityManager.h"
 #include "nsITimer.h"
 #include "nsIWritablePropertyBag2.h"
 #include "nsIXPConnect.h"
 #include "nsJSUtils.h"
 #include "nsLayoutUtils.h"
 #include "nsMathUtils.h"
 #include "nsNetUtil.h"
+#include "nsRefreshDriver.h"
 #include "nsStreamUtils.h"
 #include "ActiveLayerTracker.h"
 #include "WebGL1Context.h"
 #include "WebGL2Context.h"
 
 using namespace mozilla::layers;
 using namespace mozilla::gfx;
 
 NS_IMPL_NS_NEW_HTML_ELEMENT(Canvas)
 
 namespace mozilla {
 namespace dom {
 
+class RequestedFrameRefreshObserver : public nsARefreshObserver
+{
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RequestedFrameRefreshObserver, override)
+
+public:
+  RequestedFrameRefreshObserver(HTMLCanvasElement* const aOwningElement,
+                                nsRefreshDriver* aRefreshDriver)
+    : mRegistered(false),
+      mOwningElement(aOwningElement),
+      mRefreshDriver(aRefreshDriver)
+  {
+    MOZ_ASSERT(mOwningElement);
+  }
+
+  static already_AddRefed<DataSourceSurface>
+  CopySurface(const RefPtr<SourceSurface>& aSurface)
+  {
+    RefPtr<DataSourceSurface> data = aSurface->GetDataSurface();
+    if (!data) {
+      return nullptr;
+    }
+
+    DataSourceSurface::ScopedMap read(data, DataSourceSurface::READ);
+    if (!read.IsMapped()) {
+      return nullptr;
+    }
+
+    RefPtr<DataSourceSurface> copy =
+      Factory::CreateDataSourceSurfaceWithStride(data->GetSize(),
+                                                 data->GetFormat(),
+                                                 read.GetStride());
+    if (!copy) {
+      return nullptr;
+    }
+
+    DataSourceSurface::ScopedMap write(copy, DataSourceSurface::WRITE);
+    if (!write.IsMapped()) {
+      return nullptr;
+    }
+
+    MOZ_ASSERT(read.GetStride() == write.GetStride());
+    MOZ_ASSERT(data->GetSize() == copy->GetSize());
+    MOZ_ASSERT(data->GetFormat() == copy->GetFormat());
+
+    memcpy(write.GetData(), read.GetData(),
+           write.GetStride() * copy->GetSize().height);
+
+    return copy.forget();
+  }
+
+  void WillRefresh(TimeStamp aTime) override
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (!mOwningElement) {
+      return;
+    }
+
+    if (mOwningElement->IsWriteOnly()) {
+      return;
+    }
+
+    if (mOwningElement->IsContextCleanForFrameCapture()) {
+      return;
+    }
+
+    if (!mOwningElement->IsFrameCaptureRequested()) {
+      return;
+    }
+
+    RefPtr<SourceSurface> snapshot = mOwningElement->GetSurfaceSnapshot(nullptr);
+    if (!snapshot) {
+      return;
+    }
+
+    RefPtr<DataSourceSurface> copy = CopySurface(snapshot);
+
+    mOwningElement->SetFrameCapture(copy.forget());
+    mOwningElement->MarkContextCleanForFrameCapture();
+  }
+
+  void DetachFromRefreshDriver()
+  {
+    MOZ_ASSERT(mOwningElement);
+    MOZ_ASSERT(mRefreshDriver);
+
+    Unregister();
+    mRefreshDriver = nullptr;
+  }
+
+  void Register()
+  {
+    if (mRegistered) {
+      return;
+    }
+
+    MOZ_ASSERT(mRefreshDriver);
+    if (mRefreshDriver) {
+      mRefreshDriver->AddRefreshObserver(this, Flush_Display);
+      mRegistered = true;
+    }
+  }
+
+  void Unregister()
+  {
+    if (!mRegistered) {
+      return;
+    }
+
+    MOZ_ASSERT(mRefreshDriver);
+    if (mRefreshDriver) {
+      mRefreshDriver->RemoveRefreshObserver(this, Flush_Display);
+      mRegistered = false;
+    }
+  }
+
+private:
+  virtual ~RequestedFrameRefreshObserver()
+  {
+    MOZ_ASSERT(!mRefreshDriver);
+    MOZ_ASSERT(!mRegistered);
+  }
+
+  bool mRegistered;
+  HTMLCanvasElement* const mOwningElement;
+  RefPtr<nsRefreshDriver> mRefreshDriver;
+};
+
+// ---------------------------------------------------------------------------
+
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(HTMLCanvasPrintState, mCanvas,
                                       mContext, mCallback)
 
 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(HTMLCanvasPrintState, AddRef)
 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(HTMLCanvasPrintState, Release)
 
 HTMLCanvasPrintState::HTMLCanvasPrintState(HTMLCanvasElement* aCanvas,
                                            nsICanvasRenderingContextInternal* aContext,
@@ -111,16 +243,19 @@ HTMLCanvasElement::HTMLCanvasElement(alr
   : nsGenericHTMLElement(aNodeInfo),
     mWriteOnly(false)
 {
 }
 
 HTMLCanvasElement::~HTMLCanvasElement()
 {
   ResetPrintCallback();
+  if (mRequestedFrameRefreshObserver) {
+    mRequestedFrameRefreshObserver->DetachFromRefreshDriver();
+  }
 }
 
 NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLCanvasElement, nsGenericHTMLElement,
                                    mCurrentContext, mPrintCallback,
                                    mPrintState, mOriginalCanvas)
 
 NS_IMPL_ADDREF_INHERITED(HTMLCanvasElement, Element)
 NS_IMPL_RELEASE_INHERITED(HTMLCanvasElement, Element)
@@ -414,40 +549,35 @@ HTMLCanvasElement::CaptureStream(const O
     return nullptr;
   }
 
   if (!mCurrentContext) {
     aRv.Throw(NS_ERROR_NOT_INITIALIZED);
     return nullptr;
   }
 
-  if (mCurrentContextType != CanvasContextType::Canvas2D) {
-    WebGLContext* gl = static_cast<WebGLContext*>(mCurrentContext.get());
-    if (!gl->IsPreservingDrawingBuffer()) {
-      aRv.Throw(NS_ERROR_FAILURE);
-      return nullptr;
-    }
-  }
-
   nsRefPtr<CanvasCaptureMediaStream> stream =
     CanvasCaptureMediaStream::CreateSourceStream(window, this);
   if (!stream) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
   nsRefPtr<nsIPrincipal> principal = NodePrincipal();
   stream->CombineWithPrincipal(principal);
 
   TrackID videoTrackId = 1;
   nsresult rv = stream->Init(aFrameRate, videoTrackId);
   if (NS_FAILED(rv)) {
     aRv.Throw(rv);
     return nullptr;
   }
+
+  stream->CreateDOMTrack(videoTrackId, MediaSegment::VIDEO);
+  RegisterFrameCaptureListener(stream->FrameCaptureListener());
   return stream.forget();
 }
 
 nsresult
 HTMLCanvasElement::ExtractData(nsAString& aType,
                                const nsAString& aOptions,
                                nsIInputStream** aStream)
 {
@@ -1042,16 +1172,95 @@ HTMLCanvasElement::MarkContextCleanForFr
 }
 
 bool
 HTMLCanvasElement::IsContextCleanForFrameCapture()
 {
   return mCurrentContext && mCurrentContext->IsContextCleanForFrameCapture();
 }
 
+void
+HTMLCanvasElement::RegisterFrameCaptureListener(FrameCaptureListener* aListener)
+{
+  WeakPtr<FrameCaptureListener> listener = aListener;
+
+  if (mRequestedFrameListeners.Contains(listener)) {
+    return;
+  }
+
+  mRequestedFrameListeners.AppendElement(listener);
+
+  if (!mRequestedFrameRefreshObserver) {
+    nsIDocument* doc = OwnerDoc();
+    MOZ_RELEASE_ASSERT(doc);
+
+    nsIPresShell* shell = doc->GetShell();
+    MOZ_RELEASE_ASSERT(shell);
+
+    nsPresContext* context = shell->GetPresContext();
+    MOZ_RELEASE_ASSERT(context);
+
+    context = context->GetRootPresContext();
+    MOZ_RELEASE_ASSERT(context);
+
+    nsRefreshDriver* driver = context->RefreshDriver();
+    MOZ_RELEASE_ASSERT(driver);
+
+    mRequestedFrameRefreshObserver =
+      new RequestedFrameRefreshObserver(this, driver);
+  }
+
+  mRequestedFrameRefreshObserver->Register();
+}
+
+bool
+HTMLCanvasElement::IsFrameCaptureRequested() const
+{
+  for (WeakPtr<FrameCaptureListener> listener : mRequestedFrameListeners) {
+    if (!listener) {
+      continue;
+    }
+
+    if (listener->FrameCaptureRequested()) {
+      return true;
+    }
+  }
+  return false;
+}
+
+void
+HTMLCanvasElement::SetFrameCapture(already_AddRefed<SourceSurface> aSurface)
+{
+  RefPtr<SourceSurface> surface = aSurface;
+
+  CairoImage::Data imageData;
+  imageData.mSize = surface->GetSize();
+  imageData.mSourceSurface = surface;
+
+  nsRefPtr<CairoImage> image = new CairoImage();
+  image->SetData(imageData);
+
+  // Loop backwards to allow removing elements in the loop.
+  for (int i = mRequestedFrameListeners.Length() - 1; i >= 0; --i) {
+    WeakPtr<FrameCaptureListener> listener = mRequestedFrameListeners[i];
+    if (!listener) {
+      // listener was destroyed. Remove it from the list.
+      mRequestedFrameListeners.RemoveElementAt(i);
+      continue;
+    }
+
+    nsRefPtr<Image> imageRefCopy = image.get();
+    listener->NewFrame(imageRefCopy.forget());
+  }
+
+  if (mRequestedFrameListeners.IsEmpty()) {
+    mRequestedFrameRefreshObserver->Unregister();
+  }
+}
+
 already_AddRefed<SourceSurface>
 HTMLCanvasElement::GetSurfaceSnapshot(bool* aPremultAlpha)
 {
   if (!mCurrentContext)
     return nullptr;
 
   return mCurrentContext->GetSurfaceSnapshot(aPremultAlpha);
 }
--- a/dom/html/HTMLCanvasElement.h
+++ b/dom/html/HTMLCanvasElement.h
@@ -2,51 +2,92 @@
 /* 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/. */
 #if !defined(mozilla_dom_HTMLCanvasElement_h)
 #define mozilla_dom_HTMLCanvasElement_h
 
 #include "mozilla/Attributes.h"
+#include "mozilla/WeakPtr.h"
 #include "nsIDOMHTMLCanvasElement.h"
 #include "nsGenericHTMLElement.h"
 #include "nsGkAtoms.h"
 #include "nsSize.h"
 #include "nsError.h"
 
 #include "mozilla/gfx/Rect.h"
 
 class nsICanvasRenderingContextInternal;
 class nsITimerCallback;
 
 namespace mozilla {
 
 namespace layers {
 class CanvasLayer;
+class Image;
 class LayerManager;
 } // namespace layers
 namespace gfx {
 class SourceSurface;
 } // namespace gfx
 
 namespace dom {
 class CanvasCaptureMediaStream;
 class File;
 class FileCallback;
 class HTMLCanvasPrintState;
 class PrintCallback;
+class RequestedFrameRefreshObserver;
 
 enum class CanvasContextType : uint8_t {
   NoContext,
   Canvas2D,
   WebGL1,
   WebGL2
 };
 
+/*
+ * FrameCaptureListener is used by captureStream() as a way of getting video
+ * frames from the canvas. On a refresh driver tick after something has been
+ * drawn to the canvas since the last such tick, all registered
+ * FrameCaptureListeners whose `mFrameCaptureRequested` equals `true`,
+ * will be given a copy of the just-painted canvas.
+ * All FrameCaptureListeners get the same copy.
+ */
+class FrameCaptureListener : public SupportsWeakPtr<FrameCaptureListener>
+{
+public:
+  MOZ_DECLARE_WEAKREFERENCE_TYPENAME(FrameCaptureListener)
+
+  FrameCaptureListener()
+    : mFrameCaptureRequested(false) {}
+
+  /*
+   * Called when a frame capture is desired on next paint.
+   */
+  void RequestFrameCapture() { mFrameCaptureRequested = true; }
+
+  /*
+   * Indicates to the canvas whether or not this listener has requested a frame.
+   */
+  bool FrameCaptureRequested() const { return mFrameCaptureRequested; }
+
+  /*
+   * Interface through which new video frames will be provided while
+   * `mFrameCaptureRequested` is `true`.
+   */
+  virtual void NewFrame(already_AddRefed<layers::Image> aImage) = 0;
+
+protected:
+  virtual ~FrameCaptureListener() {}
+
+  bool mFrameCaptureRequested;
+};
+
 class HTMLCanvasElement final : public nsGenericHTMLElement,
                                 public nsIDOMHTMLCanvasElement
 {
   enum {
     DEFAULT_CANVAS_WIDTH = 300,
     DEFAULT_CANVAS_HEIGHT = 150
   };
 
@@ -166,16 +207,39 @@ public:
   /*
    * Returns true if the canvas context content is guaranteed to be opaque
    * across its entire area.
    */
   bool GetIsOpaque();
 
   virtual already_AddRefed<gfx::SourceSurface> GetSurfaceSnapshot(bool* aPremultAlpha = nullptr);
 
+  /*
+   * Register a FrameCaptureListener with this canvas.
+   * The canvas hooks into the RefreshDriver while there are
+   * FrameCaptureListeners registered.
+   * The registered FrameCaptureListeners are stored as WeakPtrs, thus it's the
+   * caller's responsibility to keep them alive. Once a registered
+   * FrameCaptureListener is destroyed it will be automatically deregistered.
+   */
+  void RegisterFrameCaptureListener(FrameCaptureListener* aListener);
+
+  /*
+   * Returns true when there is at least one registered FrameCaptureListener
+   * that has requested a frame capture.
+   */
+  bool IsFrameCaptureRequested() const;
+
+  /*
+   * Called by the RefreshDriver hook when a frame has been captured.
+   * Makes a copy of the provided surface and hands it to all
+   * FrameCaptureListeners having requested frame capture.
+   */
+  void SetFrameCapture(already_AddRefed<gfx::SourceSurface> aSurface);
+
   virtual bool ParseAttribute(int32_t aNamespaceID,
                                 nsIAtom* aAttribute,
                                 const nsAString& aValue,
                                 nsAttrValue& aResult) override;
   nsChangeHint GetAttributeChangeHint(const nsIAtom* aAttribute, int32_t aModType) const override;
 
   // SetAttr override.  C++ is stupid, so have to override both
   // overloaded methods.
@@ -248,16 +312,18 @@ protected:
                             nsISupports** aResult);
   void CallPrintCallback();
 
   CanvasContextType mCurrentContextType;
   nsRefPtr<HTMLCanvasElement> mOriginalCanvas;
   nsRefPtr<PrintCallback> mPrintCallback;
   nsCOMPtr<nsICanvasRenderingContextInternal> mCurrentContext;
   nsRefPtr<HTMLCanvasPrintState> mPrintState;
+  nsTArray<WeakPtr<FrameCaptureListener>> mRequestedFrameListeners;
+  nsRefPtr<RequestedFrameRefreshObserver> mRequestedFrameRefreshObserver;
 
 public:
   // Record whether this canvas should be write-only or not.
   // We set this when script paints an image from a different origin.
   // We also transitively set it when script paints a canvas which
   // is itself write-only.
   bool                     mWriteOnly;
 
--- a/dom/media/CanvasCaptureMediaStream.cpp
+++ b/dom/media/CanvasCaptureMediaStream.cpp
@@ -4,302 +4,179 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "CanvasCaptureMediaStream.h"
 #include "DOMMediaStream.h"
 #include "gfxPlatform.h"
 #include "ImageContainer.h"
 #include "MediaStreamGraph.h"
 #include "mozilla/dom/CanvasCaptureMediaStreamBinding.h"
-#include "mozilla/dom/HTMLCanvasElement.h"
 #include "mozilla/gfx/2D.h"
-#include "mozilla/Mutex.h"
+#include "mozilla/Atomics.h"
 #include "nsContentUtils.h"
 
 using namespace mozilla::layers;
 using namespace mozilla::gfx;
 
 namespace mozilla {
 namespace dom {
 
 class OutputStreamDriver::StreamListener : public MediaStreamListener
 {
 public:
   explicit StreamListener(OutputStreamDriver* aDriver,
+                          TrackID aTrackId,
                           SourceMediaStream* aSourceStream)
-    : mSourceStream(aSourceStream)
-    , mMutex("CanvasCaptureMediaStream::OSD::StreamListener")
-    , mDriver(aDriver)
+    : mEnded(false)
+    , mSourceStream(aSourceStream)
+    , mTrackId(aTrackId)
+    , mMutex("CanvasCaptureMediaStream OutputStreamDriver::StreamListener")
+    , mImage(nullptr)
   {
-    MOZ_ASSERT(mDriver);
     MOZ_ASSERT(mSourceStream);
   }
 
-  void Forget() {
-    MOZ_ASSERT(NS_IsMainThread());
+  void EndStream() {
+    mEnded = true;
+  }
 
+  void SetImage(const nsRefPtr<layers::Image>& aImage)
+  {
     MutexAutoLock lock(mMutex);
-    mDriver = nullptr;
+    mImage = aImage;
   }
 
   virtual void NotifyPull(MediaStreamGraph* aGraph, StreamTime aDesiredTime) override
   {
     // Called on the MediaStreamGraph thread.
+    StreamTime delta = aDesiredTime - mSourceStream->GetEndOfAppendedData(mTrackId);
+    if (delta > 0) {
+      MutexAutoLock lock(mMutex);
+      MOZ_ASSERT(mSourceStream);
 
-    MutexAutoLock lock(mMutex);
-    if (mDriver) {
-      mDriver->NotifyPull(aDesiredTime);
-    } else {
-      // The DOM stream is dead, let's end it
+      nsRefPtr<Image> image = mImage;
+      IntSize size = image ? image->GetSize() : IntSize(0, 0);
+      VideoSegment segment;
+      segment.AppendFrame(image.forget(), delta, size);
+
+      mSourceStream->AppendToTrack(mTrackId, &segment);
+    }
+
+    if (mEnded) {
       mSourceStream->EndAllTrackAndFinish();
     }
   }
 
 protected:
   ~StreamListener() { }
 
 private:
-  nsRefPtr<SourceMediaStream> mSourceStream;
+  Atomic<bool> mEnded;
+  const nsRefPtr<SourceMediaStream> mSourceStream;
+  const TrackID mTrackId;
 
-  // The below members are protected by mMutex.
   Mutex mMutex;
-  // This is a raw pointer to avoid a reference cycle with OutputStreamDriver.
-  // Accessed on main and MediaStreamGraph threads, set on main thread.
-  OutputStreamDriver* mDriver;
+  // The below members are protected by mMutex.
+  nsRefPtr<layers::Image> mImage;
 };
 
-OutputStreamDriver::OutputStreamDriver(CanvasCaptureMediaStream* aDOMStream,
+OutputStreamDriver::OutputStreamDriver(SourceMediaStream* aSourceStream,
                                        const TrackID& aTrackId)
-  : mDOMStream(aDOMStream)
-  , mSourceStream(nullptr)
-  , mStarted(false)
-  , mStreamListener(nullptr)
-  , mTrackId(aTrackId)
-  , mMutex("CanvasCaptureMediaStream::OutputStreamDriver")
-  , mImage(nullptr)
+  : FrameCaptureListener()
+  , mSourceStream(aSourceStream)
+  , mStreamListener(new StreamListener(this, aTrackId, aSourceStream))
 {
-  MOZ_ASSERT(mDOMStream);
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(mSourceStream);
+  mSourceStream->AddListener(mStreamListener);
+  mSourceStream->AddTrack(aTrackId, 0, new VideoSegment());
+  mSourceStream->AdvanceKnownTracksTime(STREAM_TIME_MAX);
+  mSourceStream->SetPullEnabled(true);
+
+  // All CanvasCaptureMediaStreams shall at least get one frame.
+  mFrameCaptureRequested = true;
 }
 
 OutputStreamDriver::~OutputStreamDriver()
 {
+  MOZ_ASSERT(NS_IsMainThread());
   if (mStreamListener) {
     // MediaStreamGraph will keep the listener alive until it can finish the
     // stream on the next NotifyPull().
-    mStreamListener->Forget();
-  }
-}
-
-nsresult
-OutputStreamDriver::Start()
-{
-  if (mStarted) {
-    return NS_ERROR_ALREADY_INITIALIZED;
-  }
-
-  MOZ_ASSERT(mDOMStream);
-
-  mDOMStream->CreateDOMTrack(mTrackId, MediaSegment::VIDEO);
-
-  mSourceStream = mDOMStream->GetStream()->AsSourceStream();
-  MOZ_ASSERT(mSourceStream);
-
-  mStreamListener = new StreamListener(this, mSourceStream);
-  mSourceStream->AddListener(mStreamListener);
-  mSourceStream->AddTrack(mTrackId, 0, new VideoSegment());
-  mSourceStream->AdvanceKnownTracksTime(STREAM_TIME_MAX);
-  mSourceStream->SetPullEnabled(true);
-
-  // Run StartInternal() in stable state to allow it to directly capture a frame
-  nsCOMPtr<nsIRunnable> runnable =
-    NS_NewRunnableMethod(this, &OutputStreamDriver::StartInternal);
-  nsContentUtils::RunInStableState(runnable.forget());
-
-  mStarted = true;
-  return NS_OK;
-}
-
-void
-OutputStreamDriver::ForgetDOMStream()
-{
-  if (mStreamListener) {
-    mStreamListener->Forget();
-  }
-  mDOMStream = nullptr;
-}
-
-void
-OutputStreamDriver::AppendToTrack(StreamTime aDuration)
-{
-  MOZ_ASSERT(mSourceStream);
-
-  MutexAutoLock lock(mMutex);
-
-  nsRefPtr<Image> image = mImage;
-  IntSize size = image ? image->GetSize() : IntSize(0, 0);
-  VideoSegment segment;
-  segment.AppendFrame(image.forget(), aDuration, size);
-
-  mSourceStream->AppendToTrack(mTrackId, &segment);
-}
-
-void
-OutputStreamDriver::NotifyPull(StreamTime aDesiredTime)
-{
-  StreamTime delta = aDesiredTime - mSourceStream->GetEndOfAppendedData(mTrackId);
-  if (delta > 0) {
-    // nullptr images are allowed
-    AppendToTrack(delta);
+    mStreamListener->EndStream();
   }
 }
 
 void
-OutputStreamDriver::SetImage(Image* aImage)
+OutputStreamDriver::SetImage(const nsRefPtr<layers::Image>& aImage)
 {
-  MutexAutoLock lock(mMutex);
-  mImage = aImage;
+  if (mStreamListener) {
+    mStreamListener->SetImage(aImage);
+  }
 }
 
 // ----------------------------------------------------------------------
 
 class TimerDriver : public OutputStreamDriver
-                  , public nsITimerCallback
 {
 public:
-  explicit TimerDriver(CanvasCaptureMediaStream* aDOMStream,
+  explicit TimerDriver(SourceMediaStream* aSourceStream,
                        const double& aFPS,
                        const TrackID& aTrackId)
-    : OutputStreamDriver(aDOMStream, aTrackId)
+    : OutputStreamDriver(aSourceStream, aTrackId)
     , mFPS(aFPS)
     , mTimer(nullptr)
   {
-  }
-
-  void ForgetDOMStream() override
-  {
-    if (mTimer) {
-      mTimer->Cancel();
-      mTimer = nullptr;
-    }
-    OutputStreamDriver::ForgetDOMStream();
-  }
-
-  nsresult
-  TakeSnapshot()
-  {
-    // mDOMStream can't be killed while we're on main thread
-    MOZ_ASSERT(NS_IsMainThread());
-    MOZ_ASSERT(DOMStream());
-
-    if (!DOMStream()->Canvas()) {
-      // DOMStream's canvas pointer was garbage collected. We can abort now.
-      return NS_ERROR_NOT_AVAILABLE;
-    }
-    MOZ_ASSERT(DOMStream()->Canvas());
-
-    if (DOMStream()->Canvas()->IsWriteOnly()) {
-      return NS_ERROR_DOM_SECURITY_ERR;
-    }
-
-    // Pass `nullptr` to force alpha-premult.
-    RefPtr<SourceSurface> snapshot = DOMStream()->Canvas()->GetSurfaceSnapshot(nullptr);
-    if (!snapshot) {
-      return NS_ERROR_FAILURE;
-    }
-
-    RefPtr<DataSourceSurface> data = snapshot->GetDataSurface();
-    if (!data) {
-      return NS_ERROR_FAILURE;
-    }
-
-    RefPtr<DataSourceSurface> copy;
-
-    {
-      DataSourceSurface::ScopedMap read(data, DataSourceSurface::READ);
-      if (!read.IsMapped()) {
-        return NS_ERROR_FAILURE;
-      }
-
-      copy = Factory::CreateDataSourceSurfaceWithStride(data->GetSize(),
-                                                        data->GetFormat(),
-                                                        read.GetStride());
-      if (!copy) {
-        return NS_ERROR_FAILURE;
-      }
-
-      DataSourceSurface::ScopedMap write(copy, DataSourceSurface::WRITE);
-      if (!write.IsMapped()) {
-        return NS_ERROR_FAILURE;
-      }
-
-      MOZ_ASSERT(read.GetStride() == write.GetStride());
-      MOZ_ASSERT(data->GetSize() == copy->GetSize());
-      MOZ_ASSERT(data->GetFormat() == copy->GetFormat());
-
-      memcpy(write.GetData(), read.GetData(),
-             write.GetStride() * copy->GetSize().height);
-    }
-
-    CairoImage::Data imageData;
-    imageData.mSize = copy->GetSize();
-    imageData.mSourceSurface = copy;
-
-    RefPtr<CairoImage> image = new layers::CairoImage();
-    image->SetData(imageData);
-
-    SetImage(image);
-    return NS_OK;
-  }
-
-  NS_IMETHODIMP
-  Notify(nsITimer* aTimer) override
-  {
-    nsresult rv = TakeSnapshot();
-    if (NS_FAILED(rv)) {
-      aTimer->Cancel();
-    }
-    return rv;
-  }
-
-  virtual void RequestFrame() override
-  {
-    TakeSnapshot();
-  }
-
-  NS_DECL_ISUPPORTS_INHERITED
-
-protected:
-  virtual ~TimerDriver() {}
-
-  virtual void StartInternal() override
-  {
-    // Always capture at least one frame.
-    DebugOnly<nsresult> rv = TakeSnapshot();
-    MOZ_ASSERT(NS_SUCCEEDED(rv));
-
     if (mFPS == 0.0) {
       return;
     }
 
     mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
     if (!mTimer) {
       return;
     }
-    mTimer->InitWithCallback(this, int(1000 / mFPS), nsITimer::TYPE_REPEATING_SLACK);
+    mTimer->InitWithFuncCallback(&TimerTick, this, int(1000 / mFPS), nsITimer::TYPE_REPEATING_SLACK);
+  }
+
+  static void TimerTick(nsITimer* aTimer, void* aClosure)
+  {
+    MOZ_ASSERT(aClosure);
+    TimerDriver* driver = static_cast<TimerDriver*>(aClosure);
+
+    driver->RequestFrameCapture();
   }
 
+  void NewFrame(already_AddRefed<Image> aImage) override
+  {
+    nsRefPtr<Image> image = aImage;
+
+    if (!mFrameCaptureRequested) {
+      return;
+    }
+
+    mFrameCaptureRequested = false;
+    SetImage(image.forget());
+  }
+
+  void Forget() override
+  {
+    if (mTimer) {
+      mTimer->Cancel();
+      mTimer = nullptr;
+    }
+  }
+
+protected:
+  virtual ~TimerDriver() {}
+
 private:
   const double mFPS;
   nsCOMPtr<nsITimer> mTimer;
 };
 
-NS_IMPL_ADDREF_INHERITED(TimerDriver, OutputStreamDriver)
-NS_IMPL_RELEASE_INHERITED(TimerDriver, OutputStreamDriver)
-NS_IMPL_QUERY_INTERFACE(TimerDriver, nsITimerCallback)
-
 // ----------------------------------------------------------------------
 
 NS_IMPL_CYCLE_COLLECTION_INHERITED(CanvasCaptureMediaStream, DOMMediaStream,
                                    mCanvas)
 
 NS_IMPL_ADDREF_INHERITED(CanvasCaptureMediaStream, DOMMediaStream)
 NS_IMPL_RELEASE_INHERITED(CanvasCaptureMediaStream, DOMMediaStream)
 
@@ -310,59 +187,67 @@ CanvasCaptureMediaStream::CanvasCaptureM
   : mCanvas(aCanvas)
   , mOutputStreamDriver(nullptr)
 {
 }
 
 CanvasCaptureMediaStream::~CanvasCaptureMediaStream()
 {
   if (mOutputStreamDriver) {
-    mOutputStreamDriver->ForgetDOMStream();
+    mOutputStreamDriver->Forget();
   }
 }
 
 JSObject*
 CanvasCaptureMediaStream::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return dom::CanvasCaptureMediaStreamBinding::Wrap(aCx, this, aGivenProto);
 }
 
 void
 CanvasCaptureMediaStream::RequestFrame()
 {
+  MOZ_ASSERT(mOutputStreamDriver);
   if (mOutputStreamDriver) {
-    mOutputStreamDriver->RequestFrame();
+    mOutputStreamDriver->RequestFrameCapture();
   }
 }
 
 nsresult
 CanvasCaptureMediaStream::Init(const dom::Optional<double>& aFPS,
                                const TrackID& aTrackId)
 {
   if (!aFPS.WasPassed()) {
     // TODO (Bug 1152298): Implement a real AutoDriver.
     // We use a 30FPS TimerDriver for now.
-    mOutputStreamDriver = new TimerDriver(this, 30.0, aTrackId);
+    mOutputStreamDriver = new TimerDriver(GetStream()->AsSourceStream(), 30.0, aTrackId);
   } else if (aFPS.Value() < 0) {
     return NS_ERROR_ILLEGAL_VALUE;
   } else {
     // Cap frame rate to 60 FPS for sanity
     double fps = std::min(60.0, aFPS.Value());
-    mOutputStreamDriver = new TimerDriver(this, fps, aTrackId);
+    mOutputStreamDriver =
+      new TimerDriver(GetStream()->AsSourceStream(), fps, aTrackId);
   }
-  return mOutputStreamDriver->Start();
+  return NS_OK;
 }
 
 already_AddRefed<CanvasCaptureMediaStream>
 CanvasCaptureMediaStream::CreateSourceStream(nsIDOMWindow* aWindow,
                                              HTMLCanvasElement* aCanvas)
 {
   nsRefPtr<CanvasCaptureMediaStream> stream = new CanvasCaptureMediaStream(aCanvas);
   MediaStreamGraph* graph =
     MediaStreamGraph::GetInstance(MediaStreamGraph::SYSTEM_THREAD_DRIVER,
                                   AudioChannel::Normal);
   stream->InitSourceStream(aWindow, graph);
   return stream.forget();
 }
 
+FrameCaptureListener*
+CanvasCaptureMediaStream::FrameCaptureListener()
+{
+  return mOutputStreamDriver;
+}
+
 } // namespace dom
 } // namespace mozilla
 
--- a/dom/media/CanvasCaptureMediaStream.h
+++ b/dom/media/CanvasCaptureMediaStream.h
@@ -2,98 +2,118 @@
 /* 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_CanvasCaptureMediaStream_h_
 #define mozilla_dom_CanvasCaptureMediaStream_h_
 
 #include "DOMMediaStream.h"
+#include "mozilla/dom/HTMLCanvasElement.h"
 #include "StreamBuffer.h"
 
 namespace mozilla {
 class DOMMediaStream;
 class MediaStreamListener;
 class SourceMediaStream;
 
 namespace layers {
 class Image;
 } // namespace layers
 
 namespace dom {
 class CanvasCaptureMediaStream;
 class HTMLCanvasElement;
+class OutputStreamFrameListener;
 
-class OutputStreamDriver
+/*
+ * The CanvasCaptureMediaStream is a MediaStream subclass that provides a video
+ * track containing frames from a canvas. See an architectural overview below.
+ *
+ * ----------------------------------------------------------------------------
+ *     === Main Thread ===              __________________________
+ *                                     |                          |
+ *                                     | CanvasCaptureMediaStream |
+ *                                     |__________________________|
+ *                                                  |
+ *                                                  | RequestFrame()
+ *                                                  v
+ *                                       ________________________
+ *  ________   FrameCaptureRequested?   |                        |
+ * |        | ------------------------> |   OutputStreamDriver   |
+ * | Canvas |  SetFrameCapture()        | (FrameCaptureListener) |
+ * |________| ------------------------> |________________________|
+ *                                                  |
+ *                                                  | SetImage()
+ *                                                  v
+ *                                         ___________________
+ *                                        |   StreamListener  |
+ * ---------------------------------------| (All image access |----------------
+ *     === MediaStreamGraph Thread ===    |   Mutex Guarded)  |
+ *                                        |___________________|
+ *                                              ^       |
+ *                                 NotifyPull() |       | AppendToTrack()
+ *                                              |       v
+ *                                      ___________________________
+ *                                     |                           |
+ *                                     |  MSG / SourceMediaStream  |
+ *                                     |___________________________|
+ * ----------------------------------------------------------------------------
+ */
+
+/*
+ * Base class for drivers of the output stream.
+ * It is up to each sub class to implement the NewFrame() callback of
+ * FrameCaptureListener.
+ */
+class OutputStreamDriver : public FrameCaptureListener
 {
 public:
-  OutputStreamDriver(CanvasCaptureMediaStream* aDOMStream,
+  OutputStreamDriver(SourceMediaStream* aSourceStream,
                      const TrackID& aTrackId);
 
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OutputStreamDriver);
 
-  nsresult Start();
-
-  virtual void ForgetDOMStream();
+  /*
+   * Sub classes can SetImage() to update the image being appended to the
+   * output stream. It will be appended on the next NotifyPull from MSG.
+   */
+  void SetImage(const nsRefPtr<layers::Image>& aImage);
 
-  virtual void RequestFrame() { }
-
-  CanvasCaptureMediaStream* DOMStream() const { return mDOMStream; }
+  /*
+   * Makes sure any internal resources this driver is holding that may create
+   * reference cycles are released.
+   */
+  virtual void Forget() {}
 
 protected:
   virtual ~OutputStreamDriver();
   class StreamListener;
 
-  /*
-   * Appends mImage to video track for the desired duration.
-   */
-  void AppendToTrack(StreamTime aDuration);
-  void NotifyPull(StreamTime aDesiredTime);
-
-  /*
-   * Sub classes can SetImage() to update the image being appended to the
-   * output stream. It will be appended on the next NotifyPull from MSG.
-   */
-  void SetImage(layers::Image* aImage);
-
-  /*
-   * Called in main thread stable state to initialize sub classes.
-   */
-  virtual void StartInternal() = 0;
-
 private:
-  // This is a raw pointer to avoid a reference cycle between OutputStreamDriver
-  // and CanvasCaptureMediaStream. ForgetDOMStream() will be called by
-  // ~CanvasCaptureMediaStream() to make sure we don't do anything illegal.
-  CanvasCaptureMediaStream* mDOMStream;
   nsRefPtr<SourceMediaStream> mSourceStream;
-  bool mStarted;
   nsRefPtr<StreamListener> mStreamListener;
-  const TrackID mTrackId;
-
-  // The below members are protected by mMutex.
-  Mutex mMutex;
-  nsRefPtr<layers::Image> mImage;
 };
 
-class CanvasCaptureMediaStream: public DOMMediaStream
+class CanvasCaptureMediaStream : public DOMMediaStream
 {
 public:
   explicit CanvasCaptureMediaStream(HTMLCanvasElement* aCanvas);
 
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(CanvasCaptureMediaStream, DOMMediaStream)
 
   nsresult Init(const dom::Optional<double>& aFPS, const TrackID& aTrackId);
 
   virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   // WebIDL
   HTMLCanvasElement* Canvas() const { return mCanvas; }
   void RequestFrame();
+  dom::FrameCaptureListener* FrameCaptureListener();
 
   /**
    * Create a CanvasCaptureMediaStream whose underlying stream is a SourceMediaStream.
    */
   static already_AddRefed<CanvasCaptureMediaStream>
   CreateSourceStream(nsIDOMWindow* aWindow,
                      HTMLCanvasElement* aCanvas);